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