1#!/usr/bin/env python 2 3# Copyright (C) 2012, Dhiru Kholia <dhiru@openwall.com> 4# Copyright (C) 2015, Dhiru Kholia <dhiru@openwall.com> 5# 6# Modified for JtR 7# 8# Copyright (C) 2011, Jeff Forcier <jeff@bitprophet.org> 9# 10# This file is part of ssh. 11# 12# 'ssh' is free software; you can redistribute it and/or modify it under the 13# terms of the GNU Lesser General Public License as published by the Free 14# Software Foundation; either version 2.1 of the License, or (at your option) 15# any later version. 16# 17# 'ssh' is distrubuted in the hope that it will be useful, but WITHOUT ANY 18# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 19# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 20# details. 21# 22# You should have received a copy of the GNU Lesser General Public License 23# along with 'ssh'; if not, write to the Free Software Foundation, Inc., 24# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 25 26import base64 27import sys 28import binascii 29from struct import unpack 30 31DES3 = 0 32AES = 1 33AES_256 = 2 34# known encryption types for private key files: 35CIPHER_TABLE = { 36 'AES-128-CBC': {'cipher': AES, 'keysize': 16, 'blocksize': 16, 'mode': "AES.MODE_CBC"}, 37 'DES-EDE3-CBC': {'cipher': DES3, 'keysize': 24, 'blocksize': 8, 'mode': "DES3.MODE_CBC"}, 38 'AES-256-CBC': {'cipher': AES_256, 'keysize': 32, 'blocksize': 16, 'mode': "AES.MODE_CBC"}, 39 'AES-192-CBC': {'cipher': AES, 'keysize': 24, 'blocksize': 16, 'mode': "AES.MODE_CBC"}, 40} 41 42 43def read_private_key(filename): 44 """ 45 Read an SSH2-format private key file, looking for a string of the type 46 C{"BEGIN xxx PRIVATE KEY"} for some C{xxx}, base64-decode the text we 47 find, and return it as a string. 48 """ 49 try: 50 f = open(filename, 'r') 51 except IOError: 52 e = sys.exc_info()[1] 53 sys.stdout.write("%s\n" % str(e)) 54 return 55 56 lines = f.readlines() 57 all_lines = ''.join(lines) 58 ktype = -1 59 tag = None 60 if "BEGIN RSA PRIVATE" in all_lines: 61 tag = "RSA" 62 ktype = 0 63 elif "-----BEGIN OPENSSH PRIVATE KEY-----" in all_lines: 64 # new private key format for OpenSSH (automatically enabled for 65 # keys using ed25519 signatures), ed25519 stuff is not supported 66 # yet! 67 ktype = 2 # bcrypt pbkdf + aes-256-cbc 68 tag = "OPENSSH" 69 elif "-----BEGIN DSA PRIVATE KEY-----" in all_lines: 70 ktype = 1 71 tag = "DSA" 72 elif "-----BEGIN EC PRIVATE KEY-----" in all_lines: 73 ktype = 3 74 tag = "EC" 75 76 if not tag: 77 sys.stderr.write("[%s] couldn't parse keyfile\n" % filename) 78 return 79 80 start = 0 81 while (start < len(lines)) and ((lines[start].strip() != '-----BEGIN ' + tag + ' PRIVATE KEY-----') and (lines[start].strip() != '-----BEGIN OPENSSH PRIVATE KEY-----')): 82 start += 1 83 if start >= len(lines): 84 sys.stderr.write("%s is not a valid private key file\n" % f.name) 85 return 86 87 # parse any headers first 88 headers = {} 89 start += 1 90 while start < len(lines): 91 l = lines[start].split(': ') 92 if len(l) == 1: 93 break 94 headers[l[0].lower()] = l[1].strip() 95 start += 1 96 # find end 97 end = start 98 while (lines[end].strip() != '-----END ' + tag + ' PRIVATE KEY-----') and (end < len(lines)): 99 end += 1 100 # if we trudged to the end of the file, just try to cope. 101 try: 102 data = ''.join(lines[start:end]).encode() 103 data = base64.decodestring(data) 104 except base64.binascii.Error: 105 e = sys.exc_info()[1] 106 raise Exception('base64 decoding error: ' + str(e)) 107 108 if 'proc-type' not in headers and ktype != 2: # unencrypted key file? 109 sys.stderr.write("%s has no password!\n" % f.name) 110 return None 111 112 try: 113 encryption_type, saltstr = headers['dek-info'].split(',') 114 except: 115 if ktype != 2: 116 raise Exception('Can\'t parse DEK-info in private key file') 117 else: 118 encryption_type = "AES-256-CBC" 119 saltstr = "fefe" # dummy value, not used 120 if encryption_type not in CIPHER_TABLE: 121 raise Exception('Unknown private key cipher "%s"' % encryption_type) 122 123 cipher = CIPHER_TABLE[encryption_type]['cipher'] 124 keysize = CIPHER_TABLE[encryption_type]['keysize'] 125 # mode = CIPHER_TABLE[encryption_type]['mode'] 126 salt = binascii.unhexlify(saltstr) 127 AUTH_MAGIC = b"openssh-key-v1" 128 if ktype == 2: # bcrypt_pbkdf format, see "sshkey_private_to_blob2" in sshkey.c 129 salt_length = 16 # fixed value in sshkey.c 130 # find offset to salt 131 offset = 0 132 if not data.startswith(AUTH_MAGIC): 133 raise Exception('Missing AUTH_MAGIC!') 134 offset = offset + len(AUTH_MAGIC) + 1 # sizeof(AUTH_MAGIC) 135 length = unpack(">I", data[offset:offset+4])[0] # ciphername length 136 if length > 32: # weak sanity check 137 raise Exception('Unknown ciphername!') 138 offset = offset + 4 + length 139 length = unpack(">I", data[offset:offset+4])[0] # kdfname length 140 offset = offset + 4 + length 141 length = unpack(">I", data[offset:offset+4])[0] # kdf length 142 salt_offset = offset + 4 + 4 # extra "4" to skip over salt length field 143 # print(salt_offset) # this should be 47, always? 144 # find offset to check bytes 145 offset = offset + 4 + length # number of keys 146 offset = offset + 4 # pubkey blob 147 length = unpack(">I", data[offset:offset+4])[0] # pubkey length 148 offset = offset + 4 + length 149 offset = offset + 4 # skip over length of "encrypted" blob 150 if offset > len(data): 151 raise Exception('Internal error in offset calculation!') 152 ciphertext_begin_offset = offset 153 saltstr = data[salt_offset:salt_offset+salt_length].encode("hex") 154 # rounds value appears after salt 155 rounds_offset = salt_offset + salt_length 156 rounds = data[rounds_offset: rounds_offset+4] 157 rounds = unpack(">I", rounds)[0] 158 if rounds == 0: 159 rounds == 16 160 161 data = binascii.hexlify(data).decode("ascii") 162 if keysize == 24 and encryption_type == "AES-192-CBC" and (ktype == 0 or ktype == 1): # RSA, DSA keys using AES-192 163 hashline = "%s:$sshng$%s$%s$%s$%s$%s" % (f.name, 4, len(saltstr) // 2, 164 saltstr, len(data) // 2, data) 165 elif keysize == 32 and encryption_type == "AES-256-CBC" and (ktype == 0 or ktype == 1): # RSA, DSA keys using AES-256 166 hashline = "%s:$sshng$%s$%s$%s$%s$%s" % (f.name, 5, len(saltstr) // 2, 167 saltstr, len(data) // 2, data) 168 elif keysize == 24: 169 hashline = "%s:$sshng$%s$%s$%s$%s$%s" % (f.name, 0, # 0 -> 3DES 170 len(salt), saltstr, len(data) // 2, data) 171 elif keysize == 16 and (ktype == 0 or ktype == 1): # RSA, DSA keys using AES-128 172 hashline = "%s:$sshng$%s$%s$%s$%s$%s" % (f.name, 1, len(saltstr) // 2, 173 saltstr, len(data) // 2, data) 174 elif keysize == 16 and ktype == 3: # EC keys using AES-128 175 hashline = "%s:$sshng$%s$%s$%s$%s$%s" % (f.name, 3, len(saltstr) // 2, 176 saltstr, len(data) // 2, data) 177 elif keysize == 32 and ktype == 2: # bcrypt pbkdf + aes-256-cbc 178 hashline = "%s:$sshng$%s$%s$%s$%s$%s$%d$%d" % (f.name, 2, len(saltstr) // 2, 179 saltstr, len(data) // 2, data, rounds, ciphertext_begin_offset) 180 else: 181 sys.stderr.write("%s uses unsupported cipher, please file a bug!\n" % f.name) 182 return None 183 184 sys.stdout.write("%s\n" % hashline) 185 186 187if __name__ == "__main__": 188 if len(sys.argv) < 2: 189 sys.stdout.write("Usage: %s <RSA/DSA/EC/OpenSSH private key file(s)>\n" % 190 sys.argv[0]) 191 192 for filename in sys.argv[1:]: 193 read_private_key(filename) 194