From 3d4c3ad201c289253f7f2c69b91237e16b91b7e9 Mon Sep 17 00:00:00 2001 From: Pavel Panteleev Date: Mon, 25 Apr 2022 19:32:24 +0400 Subject: [PATCH 1/3] Decrypt keys with paramiko --- README.md | 4 ++- bw_add_sshkeys.py | 64 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 87a3577..37c2dec 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ ## Requirements * You need to have the [Bitwarden CLI tool](https://github.com/bitwarden/cli) installed and available in the `$PATH` as `bw`. * `ssh-agent` must be running in the current session. +* Optional: `paramiko` must be installed to decrypt keys. If none of your keys are encrypted, `paramiko` is not needed ## What does it do? Fetches SSH keys stored in Bitwarden vault and adds them to `ssh-agent`. @@ -21,4 +22,5 @@ Fetches SSH keys stored in Bitwarden vault and adds them to `ssh-agent`. 2. Add an new secure note to that folder. 3. Upload the private key as an attachment. 4. Add the custom field `private` (can be overridden on the command line), containing the file name of the private key attachment. -5. Repeat steps 2-4 for each subsequent key +5. Optional: If your key is encrypted with passphrase and you want it to decrypt automatically, save passphrase into custom field `passphrase` (field name can be overriden on the command line) +6. Repeat steps 2-6 for each subsequent key diff --git a/bw_add_sshkeys.py b/bw_add_sshkeys.py index 6acc636..a4b4367 100755 --- a/bw_add_sshkeys.py +++ b/bw_add_sshkeys.py @@ -135,7 +135,7 @@ def folder_items(session: str, folder_id: str) -> List[Dict[str, Any]]: return data -def add_ssh_keys(session: str, items: List[Dict[str, Any]], keyname: str) -> None: +def add_ssh_keys(session: str, items: List[Dict[str, Any]], keyname: str, pwkeyname: str) -> None: """ Function to attempt to get keys from a vault item """ @@ -144,7 +144,7 @@ def add_ssh_keys(session: str, items: List[Dict[str, Any]], keyname: str) -> Non private_key_file = [ k['value'] for k in item['fields'] - if k['name'] == keyname and k['type'] == 0 + if k['name'] == keyname ][0] except IndexError: logging.warning('No "%s" field found for item %s', keyname, item['name']) @@ -156,6 +156,21 @@ def add_ssh_keys(session: str, items: List[Dict[str, Any]], keyname: str) -> Non continue logging.debug('Private key file declared') + private_key_pw = None + try: + private_key_pw = [ + k['value'] + for k in item['fields'] + if k['name'] == pwkeyname + ][0] + logging.debug('Passphrase declared') + except IndexError: + logging.warning('No "%s" field found for item %s', pwkeyname, item['name']) + except KeyError as error: + logging.debug( + 'No key "%s" found in item %s - skipping', error.args[0], item['name'] + ) + try: private_key_id = [ k['id'] @@ -172,12 +187,12 @@ def add_ssh_keys(session: str, items: List[Dict[str, Any]], keyname: str) -> Non logging.debug('Private key ID found') try: - ssh_add(session, item['id'], private_key_id) + ssh_add(session, item['id'], private_key_id, private_key_pw) except subprocess.SubprocessError: logging.warning('Could not add key to the SSH agent') -def ssh_add(session: str, item_id: str, key_id: str) -> None: +def ssh_add(session: str, item_id: str, key_id: str, key_pw: str) -> None: """ Function to get the key contents from the Bitwarden vault """ @@ -200,10 +215,14 @@ def ssh_add(session: str, item_id: str, key_id: str) -> None: universal_newlines=True, check=True, ) - ssh_key = proc_attachment.stdout + ssh_encrypted_key = proc_attachment.stdout + if key_pw: + ssh_key = decript_key(ssh_encrypted_key, key_pw) + else: + ssh_key = ssh_encrypted_key + logging.debug("Running ssh-add") - # CAVEAT: `ssh-add` provides no useful output, even with maximum verbosity subprocess.run( ['ssh-add', '-'], @@ -214,6 +233,31 @@ def ssh_add(session: str, item_id: str, key_id: str) -> None: check=True, ) +def decript_key(encrypted_key: str, key_pw: str) -> str: + logging.debug("Trying to open key with paramiko") + import paramiko + from io import StringIO + + ssh_encrypted_key = StringIO(encrypted_key) + + # Iterating over possible key types + for pkey_class in (paramiko.RSAKey, paramiko.DSSKey, paramiko.ECDSAKey, paramiko.Ed25519Key): + try: + key = pkey_class.from_private_key(ssh_encrypted_key, key_pw) + + logging.debug("Writing decripted key in buffer") + ssh_key = StringIO() + key.write_private_key(ssh_key) + ssh_key.seek(0) + + return ssh_key.read() + except Exception as e: + saved_exception = e + if saved_exception is not None: + raise saved_exception + + + if __name__ == '__main__': @@ -240,6 +284,12 @@ if __name__ == '__main__': default='private', help='custom field name where private key filename is stored', ) + parser.add_argument( + '-p', + '--passphrasefield', + default='passphrase', + help='custom field name where key passphrase is stored' + ) return parser.parse_args() @@ -269,7 +319,7 @@ if __name__ == '__main__': items = folder_items(session, folder_id) logging.info('Attempting to add keys to ssh-agent') - add_ssh_keys(session, items, args.customfield) + add_ssh_keys(session, items, args.customfield, args.passphrasefield) except subprocess.CalledProcessError as error: if error.stderr: logging.error('`%s` error: %s', error.cmd[0], error.stderr) From ff5258af6a66520f1a6b6c2ca57b2cca51819939 Mon Sep 17 00:00:00 2001 From: Pavel Panteleev Date: Wed, 27 Apr 2022 14:21:59 +0400 Subject: [PATCH 2/3] Provide passphrase using SSH_ASKPASS --- bw_add_sshkeys.py | 40 +++++++++------------------------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/bw_add_sshkeys.py b/bw_add_sshkeys.py index a4b4367..5a9ac94 100755 --- a/bw_add_sshkeys.py +++ b/bw_add_sshkeys.py @@ -215,12 +215,12 @@ def ssh_add(session: str, item_id: str, key_id: str, key_pw: str) -> None: universal_newlines=True, check=True, ) - ssh_encrypted_key = proc_attachment.stdout + ssh_key = proc_attachment.stdout if key_pw: - ssh_key = decript_key(ssh_encrypted_key, key_pw) + envdict = dict(os.environ, DISPLAY="1", SSH_ASKPASS=os.path.realpath(__file__), SSH_KEY_PASSPHRASE=key_pw) else: - ssh_key = ssh_encrypted_key + envdict = dict(os.environ, SSH_ASKPASS_REQUIRE="never") logging.debug("Running ssh-add") # CAVEAT: `ssh-add` provides no useful output, even with maximum verbosity @@ -228,35 +228,10 @@ def ssh_add(session: str, item_id: str, key_id: str, key_pw: str) -> None: ['ssh-add', '-'], input=ssh_key, # Works even if ssh-askpass is not installed - env=dict(os.environ, SSH_ASKPASS_REQUIRE="never"), + env=envdict, universal_newlines=True, check=True, - ) - -def decript_key(encrypted_key: str, key_pw: str) -> str: - logging.debug("Trying to open key with paramiko") - import paramiko - from io import StringIO - - ssh_encrypted_key = StringIO(encrypted_key) - - # Iterating over possible key types - for pkey_class in (paramiko.RSAKey, paramiko.DSSKey, paramiko.ECDSAKey, paramiko.Ed25519Key): - try: - key = pkey_class.from_private_key(ssh_encrypted_key, key_pw) - - logging.debug("Writing decripted key in buffer") - ssh_key = StringIO() - key.write_private_key(ssh_key) - ssh_key.seek(0) - - return ssh_key.read() - except Exception as e: - saved_exception = e - if saved_exception is not None: - raise saved_exception - - + ) if __name__ == '__main__': @@ -325,4 +300,7 @@ if __name__ == '__main__': logging.error('`%s` error: %s', error.cmd[0], error.stderr) logging.debug('Error running %s', error.cmd) - main() + if os.environ.get('SSH_ASKPASS'): + print(os.environ.get('SSH_KEY_PASSPHRASE')) + else: + main() From dee9911ba77e88eb7627dfd4688fef9859240dae Mon Sep 17 00:00:00 2001 From: Pavel Panteleev Date: Wed, 27 Apr 2022 14:26:14 +0400 Subject: [PATCH 3/3] Cleanup README --- README.md | 5 ++--- bw_add_sshkeys.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 37c2dec..249726d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ ## Requirements * You need to have the [Bitwarden CLI tool](https://github.com/bitwarden/cli) installed and available in the `$PATH` as `bw`. * `ssh-agent` must be running in the current session. -* Optional: `paramiko` must be installed to decrypt keys. If none of your keys are encrypted, `paramiko` is not needed ## What does it do? Fetches SSH keys stored in Bitwarden vault and adds them to `ssh-agent`. @@ -14,7 +13,7 @@ Fetches SSH keys stored in Bitwarden vault and adds them to `ssh-agent`. ./bw_add_sshkeys.py ``` 2. Enter your Bitwarden credentials, if a Bitwarden vault session is not already set. -3. (optional) Enter your SSH keys' passphrases. +3. (optional) Enter your SSH keys' passphrases if they're not stored in your Bitwarden. ## Storing the keys in BitWarden @@ -22,5 +21,5 @@ Fetches SSH keys stored in Bitwarden vault and adds them to `ssh-agent`. 2. Add an new secure note to that folder. 3. Upload the private key as an attachment. 4. Add the custom field `private` (can be overridden on the command line), containing the file name of the private key attachment. -5. Optional: If your key is encrypted with passphrase and you want it to decrypt automatically, save passphrase into custom field `passphrase` (field name can be overriden on the command line) +5. (optional) If your key is encrypted with passphrase and you want it to decrypt automatically, save passphrase into custom field `passphrase` (field name can be overriden on the command line) 6. Repeat steps 2-6 for each subsequent key diff --git a/bw_add_sshkeys.py b/bw_add_sshkeys.py index 5a9ac94..8f6eac0 100755 --- a/bw_add_sshkeys.py +++ b/bw_add_sshkeys.py @@ -218,7 +218,7 @@ def ssh_add(session: str, item_id: str, key_id: str, key_pw: str) -> None: ssh_key = proc_attachment.stdout if key_pw: - envdict = dict(os.environ, DISPLAY="1", SSH_ASKPASS=os.path.realpath(__file__), SSH_KEY_PASSPHRASE=key_pw) + envdict = dict(os.environ, SSH_ASKPASS=os.path.realpath(__file__), SSH_KEY_PASSPHRASE=key_pw) else: envdict = dict(os.environ, SSH_ASKPASS_REQUIRE="never")