diff options
-rw-r--r-- | paramiko/__init__.py | 1 | ||||
-rw-r--r-- | paramiko/ed25519key.py | 50 | ||||
-rw-r--r-- | paramiko/transport.py | 2 | ||||
-rw-r--r-- | setup.py | 1 | ||||
-rw-r--r-- | tests/test_ed25519.key | 8 | ||||
-rw-r--r-- | tests/test_ed25519_password.key | 8 | ||||
-rw-r--r-- | tests/test_pkey.py | 19 |
7 files changed, 72 insertions, 17 deletions
diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 197f519a..d67ad62f 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -45,6 +45,7 @@ from paramiko.server import ServerInterface, SubsystemHandler, InteractiveQuery from paramiko.rsakey import RSAKey from paramiko.dsskey import DSSKey from paramiko.ecdsakey import ECDSAKey +from paramiko.ed25519key import Ed25519Key from paramiko.sftp import SFTPError, BaseSFTP from paramiko.sftp_client import SFTP, SFTPClient from paramiko.sftp_server import SFTPServer diff --git a/paramiko/ed25519key.py b/paramiko/ed25519key.py index 9638cc01..694b1e15 100644 --- a/paramiko/ed25519key.py +++ b/paramiko/ed25519key.py @@ -14,6 +14,11 @@ # along with Paramiko; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. +import bcrypt + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher + import nacl.signing import six @@ -49,7 +54,7 @@ class Ed25519Key(PKey): elif filename is not None: with open(filename, "rb") as f: data = self._read_private_key("OPENSSH", f) - signing_key = self._parse_signing_key_data(data) + signing_key = self._parse_signing_key_data(data, password) if signing_key is None and verifying_key is None: raise ValueError("need a key") @@ -58,7 +63,8 @@ class Ed25519Key(PKey): self._verifying_key = verifying_key - def _parse_signing_key_data(self, data): + def _parse_signing_key_data(self, data, password): + from paramiko.transport import Transport # We may eventually want this to be usable for other key types, as # OpenSSH moves to it, but for now this is just for Ed25519 keys. message = Message(data) @@ -70,10 +76,22 @@ class Ed25519Key(PKey): kdfoptions = message.get_string() num_keys = message.get_int() - if ciphername != "none" or kdfname != "none" or kdfoptions: - # TODO: add support for `kdfname == "bcrypt"` as documented in: - # https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key#L21-L28 - raise NotImplementedError("Encrypted keys are not implemented") + if kdfname == "none": + # kdfname of "none" must have an empty kdfoptions, the ciphername + # must be "none" and there must not be a password. + if kdfoptions or ciphername != "none" or password: + raise SSHException('Invalid key') + elif kdfname == "bcrypt": + if not password: + raise SSHException('Invalid key') + kdf = Message(kdfoptions) + bcrypt_salt = kdf.get_binary() + bcrypt_rounds = kdf.get_int() + else: + raise SSHException('Invalid key') + + if ciphername != "none" and ciphername not in Transport._cipher_info: + raise SSHException('Invalid key') public_keys = [] for _ in range(num_keys): @@ -82,7 +100,25 @@ class Ed25519Key(PKey): raise SSHException('Invalid key') public_keys.append(pubkey.get_binary()) - message = Message(unpad(message.get_binary())) + private_ciphertext = message.get_binary() + if ciphername == "none": + private_data = private_ciphertext + else: + cipher = Transport._cipher_info[ciphername] + key = bcrypt.kdf( + password=password, + salt=bcrypt_salt, + desired_key_bytes=cipher['key-size'] + cipher['block-size'], + rounds=bcrypt_rounds + ) + decryptor = Cipher( + cipher['class'](key[:cipher['key-size']]), + cipher['mode'](key[cipher['key-size']:]), + backend=default_backend() + ).decryptor() + private_data = decryptor.update(private_ciphertext) + decryptor.finalize() + + message = Message(unpad(private_data)) if message.get_int() != message.get_int(): raise SSHException('Invalid key') diff --git a/paramiko/transport.py b/paramiko/transport.py index acba0b81..7e437cc9 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -85,7 +85,7 @@ import atexit atexit.register(_join_lingering_threads) -class Transport (threading.Thread, ClosingContextManager): +class Transport(threading.Thread, ClosingContextManager): """ An SSH Transport attaches to a stream (usually a socket), negotiates an encrypted session, authenticates, and then creates stream tunnels, called @@ -74,6 +74,7 @@ setup( 'Programming Language :: Python :: 3.5', ], install_requires=[ + 'bcrypt>=3.0.0', 'cryptography>=1.1', 'pynacl', 'pyasn1>=0.1.7', diff --git a/tests/test_ed25519.key b/tests/test_ed25519.key new file mode 100644 index 00000000..eb9f94c2 --- /dev/null +++ b/tests/test_ed25519.key @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACB69SvZKJh/9VgSL0G27b5xVYa8nethH3IERbi0YqJDXwAAAKhjwAdrY8AH +awAAAAtzc2gtZWQyNTUxOQAAACB69SvZKJh/9VgSL0G27b5xVYa8nethH3IERbi0YqJDXw +AAAEA9tGQi2IrprbOSbDCF+RmAHd6meNSXBUQ2ekKXm4/8xnr1K9komH/1WBIvQbbtvnFV +hryd62EfcgRFuLRiokNfAAAAI2FsZXhfZ2F5bm9yQEFsZXhzLU1hY0Jvb2stQWlyLmxvY2 +FsAQI= +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/test_ed25519_password.key b/tests/test_ed25519_password.key new file mode 100644 index 00000000..d178aaae --- /dev/null +++ b/tests/test_ed25519_password.key @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABDaKD4ac7 +kieb+UfXaLaw68AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIOQn7fjND5ozMSV3 +CvbEtIdT73hWCMRjzS/lRdUDw50xAAAAsE8kLGyYBnl9ihJNqv378y6mO3SkzrDbWXOnK6 +ij0vnuTAvcqvWHAnyu6qBbplu/W2m55ZFeAItgaEcV2/V76sh/sAKlERqrLFyXylN0xoOW +NU5+zU08aTlbSKGmeNUU2xE/xfJq12U9XClIRuVUkUpYANxNPbmTRpVrbD3fgXMhK97Jrb +DEn8ca1IqMPiYmd/hpe5+tq3OxyRljXjCUFWTnqkp9VvUdzSTdSGZHsW9i +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/test_pkey.py b/tests/test_pkey.py index 24d78c3e..74330b8d 100644 --- a/tests/test_pkey.py +++ b/tests/test_pkey.py @@ -27,7 +27,7 @@ from binascii import hexlify from hashlib import md5 import base64 -from paramiko import RSAKey, DSSKey, ECDSAKey, Message, util +from paramiko import RSAKey, DSSKey, ECDSAKey, Ed25519Key, Message, util from paramiko.py3compat import StringIO, byte_chr, b, bytes, PY2 from tests.util import test_path @@ -112,14 +112,7 @@ TEST_KEY_BYTESTR_2 = '\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01#\x00\x00\x00\x81\x TEST_KEY_BYTESTR_3 = '\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01#\x00\x00\x00\x00ӏV\x07k%<\x1fT$E#>ғfD\x18 \x0cae#̬S#VlE\x1epvo\x17M߉DUXL<\x06\x10דw\u2bd5ٿw˟0)#y{\x10l\tPru\t\x19Π\u070e/f0yFmm\x1f' -class KeyTest (unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - +class KeyTest(unittest.TestCase): def test_1_generate_key_bytes(self): key = util.generate_key_bytes(md5, x1234, 'happy birthday', 30) exp = b'\x61\xE1\xF2\x72\xF4\xC1\xC4\x56\x15\x86\xBD\x32\x24\x98\xC0\xE9\x24\x67\x27\x80\xF4\x7B\xB3\x7D\xDA\x7D\x54\x01\x9E\x64' @@ -436,3 +429,11 @@ class KeyTest (unittest.TestCase): key = RSAKey.from_private_key_file(test_path('test_rsa.key')) comparable = TEST_KEY_BYTESTR_2 if PY2 else TEST_KEY_BYTESTR_3 self.assertEqual(str(key), comparable) + + def test_ed25519(self): + key1 = Ed25519Key.from_private_key_file(test_path('test_ed25519.key')) + key2 = Ed25519Key.from_private_key_file( + test_path('test_ed25519_password.key'), 'abc123' + ) + + self.assertNotEqual(key1.asbytes(), key2.asbytes()) |