diff options
author | Legrandin <gooksankoo@hoiptorrow.mailexpire.com> | 2011-10-03 23:33:11 +0200 |
---|---|---|
committer | Legrandin <gooksankoo@hoiptorrow.mailexpire.com> | 2011-10-03 23:33:11 +0200 |
commit | 674b80db746ab4898cfb6a5fe44a642c7cb4c946 (patch) | |
tree | ef486fcbdaeadb655b7ad551adb0d1a327e917fb | |
parent | 9cb1a2d35d916180dee8351fe6f2ddf4f6dba72d (diff) | |
download | pycrypto-674b80db746ab4898cfb6a5fe44a642c7cb4c946.tar.gz |
Added support for pass phrase and DES/3DES encrypted PEM keys, for both import and export.
-rw-r--r-- | lib/Crypto/PublicKey/RSA.py | 71 | ||||
-rw-r--r-- | lib/Crypto/SelfTest/PublicKey/test_importKey.py | 53 |
2 files changed, 117 insertions, 7 deletions
diff --git a/lib/Crypto/PublicKey/RSA.py b/lib/Crypto/PublicKey/RSA.py index 908ec68..e8b4c0a 100644 --- a/lib/Crypto/PublicKey/RSA.py +++ b/lib/Crypto/PublicKey/RSA.py @@ -166,16 +166,19 @@ class _RSAobj(pubkey.pubkey): attrs.append("private") return "<%s @0x%x %s>" % (self.__class__.__name__, id(self), ",".join(attrs)) - def exportKey(self, format='PEM'): + def exportKey(self, format='PEM', passphrase=None): """Export this RSA key. :Parameter format: The encoding to use to wrap the key. - - *'DER'* for PKCS#1 + - *'DER'* for PKCS#1. Always unencrypted. - *'OpenSSH'* for OpenSSH public keys (no private key) - - *'PEM'* for RFC1421 + - *'PEM'* for RFC1421/3. Unencrypted (default) or encrypted. :Type format: string + :Parameter passphrase: In case of PEM, the pass phrase to derive the encryption key from. + :Type passphrase: string + :Return: A string with the encoded public or private half. :Raise ValueError: When the format is unknown. @@ -209,7 +212,25 @@ class _RSAobj(pubkey.pubkey): return der.encode() if format=='PEM': pem = "-----BEGIN %s KEY-----\n" % keyType + keyobj = None + if passphrase and keyType=='RSA PRIVATE': + # We only support 3DES for encryption + import Crypto.Hash.MD5 + from Crypto.Cipher import DES3 + from Crypto.Protocol.KDF import PBKDF1 + salt = self._randfunc(8) + key = PBKDF1(passphrase, salt, 16, 1, Crypto.Hash.MD5) + key += PBKDF1(key+passphrase, salt, 8, 1, Crypto.Hash.MD5) + keyobj = DES3.new(key, Crypto.Cipher.DES3.MODE_CBC, salt) + pem += 'Proc-Type: 4,ENCRYPTED\n' + pem += 'DEK-Info: DES-EDE3-CBC,' + binascii.b2a_hex(salt).upper() + '\n\n' + binaryKey = der.encode() + if keyobj: + # Add PKCS#7-like padding + padding = keyobj.block_size-len(binaryKey)%keyobj.block_size + binaryKey = keyobj.encrypt(binaryKey+chr(padding)*padding) + # Each BASE64 line can take up to 64 characters (=48 bytes of data) chunks = [ binascii.b2a_base64(binaryKey[i:i+48]) for i in range(0, len(binaryKey), 48) ] pem += ''.join(chunks) @@ -380,23 +401,59 @@ class RSAImplementation(object): return self.construct(der[:]) raise ValueError("RSA key format is not supported") - def importKey(self, externKey): + def importKey(self, externKey, passphrase=None): """Import an RSA key (public or private half), encoded in standard form. :Parameter externKey: The RSA key to import, encoded as a string. - The key can be in DER (PKCS#1), OpenSSH or in unencrypted PEM format (RFC1421). + The key can be in DER (PKCS#1), OpenSSH or in PEM format (RFC1421/3). + In case of PEM, the key can be encrypted with DES or 3TDES according to a certain pass phrase. + Only OpenSSL-compatible pass phrases are supported. :Type externKey: string - :Raise ValueError/IndexError: - When the given key cannot be parsed. + :Parameter passphrase: + In case of an encrypted PEM key, this is the pass phrase from which the encryption key is derived. + :Type passphrase: string + + :Raise ValueError/IndexError/TypeError: + When the given key cannot be parsed (possibly because the pass phrase is wrong). """ if externKey.startswith('-----'): # This is probably a PEM encoded key lines = externKey.replace(" ",'').split() + keyobj = None + + # The encrypted PEM format + if lines[1].startswith('Proc-Type:4,ENCRYPTED'): + DEK = lines[2].split(':') + if len(DEK)!=2 or DEK[0]!='DEK-Info' or not passphrase: + raise ValueError("PEM encryption format not supported.") + algo, salt = DEK[1].split(',') + salt = binascii.a2b_hex(salt) + import Crypto.Hash.MD5 + from Crypto.Cipher import DES, DES3 + from Crypto.Protocol.KDF import PBKDF1 + if algo=="DES-CBC": + # This is EVP_BytesToKey in OpenSSL + key = PBKDF1(passphrase, salt, 8, 1, Crypto.Hash.MD5) + keyobj = DES.new(key, Crypto.Cipher.DES.MODE_CBC, salt) + elif algo=="DES-EDE3-CBC": + # Note that EVP_BytesToKey is note exactly the same as PBKDF1 + key = PBKDF1(passphrase, salt, 16, 1, Crypto.Hash.MD5) + key += PBKDF1(key+passphrase, salt, 8, 1, Crypto.Hash.MD5) + keyobj = DES3.new(key, Crypto.Cipher.DES3.MODE_CBC, salt) + else: + raise ValueError("Unsupport PEM encryption algorithm.") + lines = lines[2:] + der = binascii.a2b_base64(''.join(lines[1:-1])) + if keyobj: + der = keyobj.decrypt(der) + padding = ord(der[-1]) + der = der[:-padding] return self._importKeyDER(der) + if externKey.startswith('ssh-rsa '): # This is probably an OpenSSH key keystring = binascii.a2b_base64(externKey.split(' ')[1]) diff --git a/lib/Crypto/SelfTest/PublicKey/test_importKey.py b/lib/Crypto/SelfTest/PublicKey/test_importKey.py index 018754d..fe4fee2 100644 --- a/lib/Crypto/SelfTest/PublicKey/test_importKey.py +++ b/lib/Crypto/SelfTest/PublicKey/test_importKey.py @@ -42,6 +42,40 @@ JACAr3sJQJGxIQIgarRp+m1WSKV1MciwMaTOnbU7wxFs9DP1pva76lYBzgUCIQC9 n0CnZCJ6IZYqSt0H5N7+Q+2Ro64nuwV/OSQfM6sBwQ== -----END RSA PRIVATE KEY-----''' + # The same RSA private key as in rsaKeyPEM, but now encrypted + rsaKeyEncryptedPEM=( + + # With DES and passphrase 'test' + ('test', '''-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-CBC,AF8F9A40BD2FA2FC + +Ckl9ex1kaVEWhYC2QBmfaF+YPiR4NFkRXA7nj3dcnuFEzBnY5XULupqQpQI3qbfA +u8GYS7+b3toWWiHZivHbAAUBPDIZG9hKDyB9Sq2VMARGsX1yW1zhNvZLIiVJzUHs +C6NxQ1IJWOXzTew/xM2I26kPwHIvadq+/VaT8gLQdjdH0jOiVNaevjWnLgrn1mLP +BCNRMdcexozWtAFNNqSzfW58MJL2OdMi21ED184EFytIc1BlB+FZiGZduwKGuaKy +9bMbdb/1PSvsSzPsqW7KSSrTw6MgJAFJg6lzIYvR5F4poTVBxwBX3+EyEmShiaNY +IRX3TgQI0IjrVuLmvlZKbGWP18FXj7I7k9tSsNOOzllTTdq3ny5vgM3A+ynfAaxp +dysKznQ6P+IoqML1WxAID4aGRMWka+uArOJ148Rbj9s= +-----END RSA PRIVATE KEY-----''', + "\xAF\x8F\x9A\x40\xBD\x2F\xA2\xFC"), + + # With Triple-DES and passphrase 'rocking' + ('rocking', '''-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,C05D6C07F7FC02F6 + +w4lwQrXaVoTTJ0GgwY566htTA2/t1YlimhxkxYt9AEeCcidS5M0Wq9ClPiPz9O7F +m6K5QpM1rxo1RUE/ZyI85gglRNPdNwkeTOqit+kum7nN73AToX17+irVmOA4Z9E+ +4O07t91GxGMcjUSIFk0ucwEU4jgxRvYscbvOMvNbuZszGdVNzBTVddnShKCsy9i7 +nJbPlXeEKYi/OkRgO4PtfqqWQu5GIEFVUf9ev1QV7AvC+kyWTR1wWYnHX265jU5c +sopxQQtP8XEHIJEdd5/p1oieRcWTCNyY8EkslxDSsrf0OtZp6mZH9N+KU47cgQtt +9qGORmlWnsIoFFKcDohbtOaWBTKhkj5h6OkLjFjfU/sBeV1c+7wDT3dAy5tawXjG +YSxC7qDQIT/RECvV3+oQKEcmpEujn45wAnkTi12BH30= +-----END RSA PRIVATE KEY-----''', + "\xC0\x5D\x6C\x07\xF7\xFC\x02\xF6"), + ) + rsaPublicKeyPEM = '''-----BEGIN PUBLIC KEY----- MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAL8eJ5AKoIsjURpcEoGubZMxLD7+kT+T Lr7UkvEtFrRhDDKMtuIIq19FrL4pUIMymPMSLBn3hJLe30Dw48GQM4UCAwEAAQ== @@ -129,6 +163,16 @@ Lr7UkvEtFrRhDDKMtuIIq19FrL4pUIMymPMSLBn3hJLe30Dw48GQM4UCAwEAAQ== self.assertEqual(key.n, self.n) self.assertEqual(key.e, self.e) + def testImportKey8(self): + for t in self.rsaKeyEncryptedPEM: + key = self.rsa.importKey(t[1], t[0]) + self.failUnless(key.has_private()) + self.assertEqual(key.n, self.n) + self.assertEqual(key.e, self.e) + self.assertEqual(key.d, self.d) + self.assertEqual(key.p, self.p) + self.assertEqual(key.q, self.q) + ### def testExportKey1(self): key = self.rsa.construct([self.n, self.e, self.d, self.p, self.q, self.pInv]) @@ -157,6 +201,15 @@ Lr7UkvEtFrRhDDKMtuIIq19FrL4pUIMymPMSLBn3hJLe30Dw48GQM4UCAwEAAQ== self.assertEqual(openssh_1[0], openssh_2[0]) self.assertEqual(openssh_1[1], openssh_2[1]) + def testExportKey4(self): + key = self.rsa.construct([self.n, self.e, self.d, self.p, self.q, self.pInv]) + # Tuple with index #1 is encrypted with 3DES + t = self.rsaKeyEncryptedPEM[1] + # Force the salt being used when exporting + key._randfunc = lambda N: (t[2]*divmod(N+len(t[2]),len(t[2]))[0])[:N] + pemKey = key.exportKey("PEM", t[0]) + self.assertEqual(pemKey, t[1]) + class ImportKeyTestsSlow(ImportKeyTests): def setUp(self): self.rsa = RSA.RSAImplementation(use_fast_math=0) |