From 1f1278270df5dd452ffaa3646912f2520387a9c7 Mon Sep 17 00:00:00 2001 From: Divyansh Singh Date: Fri, 17 Mar 2023 20:33:48 +0530 Subject: [PATCH] Remove paramiko dependency (#1606) Paramiko is a encryption key parsing library. It was used for determining which ssh keys are available on the system. This removes that fairly heavy dependency at replaces it with a very basic heuristic to determine ssh key file by their first line containing `-----BEGIN(\s\w+)? PRIVATE KEY-----`. * src/vorta/utils.py: Implement `is_ssh_private_key_file`. * src/vorta/utils.py (get_private_keys): Use `is_ssh_private_key_file` instead of paramiko. Enforce `077` permissions on key files. * src/vorta/views/ssh_dialog.py : Remove paramiko. * src/vorta/views/repo_tab.py (RepoTab.init_ssh): Show filename only in `sshComboBox`. * src/vorta/views/repo_add_dialog.py (AddRepoWindow.init_ssh_key): Show filename only in `sshComboBox`. --- src/vorta/utils.py | 66 ++++++++++++++---------------- src/vorta/views/repo_add_dialog.py | 5 +-- src/vorta/views/repo_tab.py | 2 +- src/vorta/views/ssh_dialog.py | 5 --- 4 files changed, 33 insertions(+), 45 deletions(-) diff --git a/src/vorta/utils.py b/src/vorta/utils.py index 3bab52a72..774949d6b 100644 --- a/src/vorta/utils.py +++ b/src/vorta/utils.py @@ -10,12 +10,8 @@ import unicodedata from datetime import datetime as dt from functools import reduce -from typing import Any, Callable, Iterable, Optional, Tuple, TypeVar +from typing import Any, Callable, Iterable, List, Optional, Tuple, TypeVar import psutil -from paramiko import SSHException -from paramiko.ecdsakey import ECDSAKey -from paramiko.ed25519key import Ed25519Key -from paramiko.rsakey import RSAKey from PyQt5 import QtCore from PyQt5.QtCore import QFileInfo, QThread, pyqtSignal from PyQt5.QtWidgets import QApplication, QFileDialog, QSystemTrayIcon @@ -179,9 +175,19 @@ def choose_file_dialog(parent, title, want_folder=True): return dialog -def get_private_keys(): +def is_ssh_private_key_file(filepath: str) -> bool: + """Check if the file is a SSH key.""" + try: + with open(filepath, 'r') as f: + first_line = f.readline() + pattern = r'^-----BEGIN(\s\w+)? PRIVATE KEY-----' + return re.match(pattern, first_line) is not None + except UnicodeDecodeError: + return False + + +def get_private_keys() -> List[str]: """Find SSH keys in standard folder.""" - key_formats = [RSAKey, ECDSAKey, Ed25519Key] ssh_folder = os.path.expanduser('~/.ssh') @@ -191,35 +197,25 @@ def get_private_keys(): key_file = os.path.join(ssh_folder, key) if not os.path.isfile(key_file): continue - for key_format in key_formats: - try: - parsed_key = key_format.from_private_key_file(key_file) - key_details = { - 'filename': key, - 'format': parsed_key.get_name(), - 'bits': parsed_key.get_bits(), - 'fingerprint': parsed_key.get_fingerprint().hex(), - } - available_private_keys.append(key_details) - except ( - SSHException, - UnicodeDecodeError, - IsADirectoryError, - IndexError, - ValueError, - PermissionError, - NotImplementedError, - ): - logger.debug( - f'Expected error parsing file in .ssh: {key} (You can safely ignore this)', exc_info=True - ) - continue - except OSError as e: - if e.errno == errno.ENXIO: - # when key_file is a (ControlPath) socket - continue + # ignore config, known_hosts*, *.pub, etc. + if key.endswith('.pub') or key.startswith('known_hosts') or key == 'config': + continue + try: + if is_ssh_private_key_file(key_file): + if os.stat(key_file).st_mode & 0o077 == 0: + available_private_keys.append(key) else: - raise + logger.warning(f'Permissions for {key_file} are too open.') + else: + logger.debug(f'Not a private SSH key file: {key}') + except PermissionError: + logger.warning(f'Permission error while opening file: {key_file}', exc_info=True) + continue + except OSError as e: + if e.errno == errno.ENXIO: + # when key_file is a (ControlPath) socket + continue + raise return available_private_keys diff --git a/src/vorta/views/repo_add_dialog.py b/src/vorta/views/repo_add_dialog.py index dbb88a299..4c8c988ee 100644 --- a/src/vorta/views/repo_add_dialog.py +++ b/src/vorta/views/repo_add_dialog.py @@ -182,10 +182,7 @@ def init_encryption(self): def init_ssh_key(self): keys = get_private_keys() for key in keys: - self.sshComboBox.addItem( - f'{key["filename"]} ({key["format"]}:{key["fingerprint"]})', - key['filename'], - ) + self.sshComboBox.addItem(f'{key}', key) def validate(self): """Pre-flight check for valid input and borg binary.""" diff --git a/src/vorta/views/repo_tab.py b/src/vorta/views/repo_tab.py index f531f2fa0..99d3613b4 100644 --- a/src/vorta/views/repo_tab.py +++ b/src/vorta/views/repo_tab.py @@ -175,7 +175,7 @@ def init_ssh(self): self.sshComboBox.clear() self.sshComboBox.addItem(self.tr('Automatically choose SSH Key (default)'), None) for key in keys: - self.sshComboBox.addItem(f'{key["filename"]} ({key["format"]})', key['filename']) + self.sshComboBox.addItem(f'{key}', key) def toggle_available_compression(self): use_zstd = borg_compat.check('ZSTD') diff --git a/src/vorta/views/ssh_dialog.py b/src/vorta/views/ssh_dialog.py index 7720ba74b..df7402814 100644 --- a/src/vorta/views/ssh_dialog.py +++ b/src/vorta/views/ssh_dialog.py @@ -1,7 +1,4 @@ import os -from paramiko.ecdsakey import ECDSAKey -from paramiko.ed25519key import Ed25519Key -from paramiko.rsakey import RSAKey from PyQt5 import uic from PyQt5.QtCore import QProcess, Qt from PyQt5.QtWidgets import QApplication, QDialogButtonBox @@ -10,8 +7,6 @@ uifile = get_asset('UI/sshadd.ui') SSHAddUI, SSHAddBase = uic.loadUiType(uifile) -FORMAT_MAPPING = {'ed25519': Ed25519Key, 'rsa': RSAKey, 'ecdsa': ECDSAKey} - class SSHAddWindow(SSHAddBase, SSHAddUI): def __init__(self):