Created
December 16, 2020 16:38
-
-
Save adiroiban/1af3df6061a42b102a4faa94a808b056 to your computer and use it in GitHub Desktop.
Read putty private key in Python
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Code extracted from my private project, but released here as public code. | |
@classmethod | |
def _fromString_PRIVATE_PUTTY(cls, data, passphrase): | |
""" | |
Read a private Putty key. | |
Format is: | |
PuTTY-User-Key-File-2: ssh-rsa | |
Encryption: aes256-cbc | none | |
Comment: SINGLE_LINE_COMMENT | |
Public-Lines: PUBLIC_LINES | |
< base64 public part always in plain > | |
Private-Lines: 8 | |
< base64 private part > | |
Private-MAC: 1398fbfc7ce307d9ee0e42851f183f88c728398f | |
Pulic part RSA: | |
* string type (ssh-rsa) | |
* mpint e | |
* mpint n | |
Private part RSA: | |
* mpint d | |
* mpint q | |
* mpint p | |
* mpint u | |
Pulic part DSA: | |
* string type (ssh-dss) | |
* mpint p | |
* mpint q | |
* mpint g | |
* mpint v` | |
Private part DSA: | |
* mpint x | |
Private part is padded for encryption. | |
Encryption key is composed of concatenating, up to block size: | |
* uint32 sequence, starting from 0 | |
* passphrase | |
Lines are terminated by CRLF, although CR-only and LF-only are | |
tolerated on input. | |
Only version 2 is supported. | |
Version 2 was introduced in PuTTY 0.52. | |
Version 1 was an in-development format used in 0.52 snapshot | |
""" | |
lines = data.strip().splitlines() | |
key_type = lines[0][22:].strip().lower() | |
if key_type not in [b'ssh-rsa', b'ssh-dss']: | |
raise BadKeyError('Unsupported key type: %r' % key_type[:30]) | |
encryption_type = lines[1][11:].strip().lower() | |
if encryption_type == b'none': | |
if passphrase: | |
raise BadKeyError('PuTTY key not encrypted') | |
elif encryption_type != b'aes256-cbc': | |
raise BadKeyError( | |
'Unsupported encryption type: %r' % encryption_type[:30]) | |
comment = lines[2][9:].strip() | |
public_count = int(lines[3][14:].strip()) | |
base64_content = ''.join(lines[ | |
4: | |
4 + public_count | |
]) | |
public_blob = base64.decodestring(base64_content) | |
public_type, public_payload = common.getNS(public_blob) | |
if public_type.lower() != key_type: | |
raise BadKeyError( | |
'Mismatch key type. Header has %r, public has %r' % ( | |
key_type[:30], public_type[:30])) | |
# We skip 4 lines so far and the total public lines. | |
private_start_line = 4 + public_count | |
private_count = int(lines[private_start_line][15:].strip()) | |
base64_content = ''.join(lines[ | |
private_start_line + 1: | |
private_start_line + 1 + private_count | |
]) | |
private_blob = base64.decodestring(base64_content) | |
private_mac = lines[-1][12:].strip() | |
hmac_key = PUTTY_HMAC_KEY | |
encryption_key = None | |
if encryption_type == b'aes256-cbc': | |
if not passphrase: | |
raise EncryptedKeyError( | |
'Passphrase must be provided for an encrypted key.') | |
hmac_key += passphrase | |
encryption_key = cls._getPuttyAES256EncryptionKey(passphrase) | |
private_blob = AES.new( | |
encryption_key, mode=AES.MODE_CBC, IV=b'\x00' * 16).decrypt( | |
private_blob) | |
# I have no idea why these values are packed form HMAC as net strings. | |
hmac_data = ( | |
common.NS(key_type) + | |
common.NS(encryption_type) + | |
common.NS(comment) + | |
common.NS(public_blob) + | |
common.NS(private_blob) | |
) | |
hmac_key = sha1(hmac_key).digest() | |
computed_mac = hmac.new(hmac_key, hmac_data, sha1).hexdigest() | |
if private_mac != computed_mac: | |
if encryption_key: | |
raise EncryptedKeyError('Bad password or HMAC mismatch.') | |
else: | |
raise BadKeyError( | |
'HMAC mismatch: file declare %s, actual is %s' % ( | |
private_mac, computed_mac)) | |
if key_type == b'ssh-rsa': | |
e, n, _ = common.getMP(public_payload, count=2) | |
d, q, p, u, _ = common.getMP(private_blob, count=4) | |
return cls(RSA.construct((n, e, d, p, q, u))) | |
if key_type == b'ssh-dss': | |
p, q, g, y, _ = common.getMP(public_payload, count=4) | |
x, _ = common.getMP(private_blob) | |
return cls(DSA.construct((y, g, p, q, x))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment