summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--paramiko/__init__.py1
-rw-r--r--paramiko/ed25519key.py50
-rw-r--r--paramiko/transport.py2
-rw-r--r--setup.py1
-rw-r--r--tests/test_ed25519.key8
-rw-r--r--tests/test_ed25519_password.key8
-rw-r--r--tests/test_pkey.py19
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
diff --git a/setup.py b/setup.py
index 2756a76d..458916a6 100644
--- a/setup.py
+++ b/setup.py
@@ -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())