// TODO: // * utilize `crypto.create(Private|Public)Key()` and `keyObject.export()` // * handle multi-line header values (OpenSSH)? // * more thorough validation? 'use strict' require('../buffer-polyfill.js') const { createDecipheriv, createECDH, createHash, createHmac, createSign, createVerify, getCiphers, sign: sign_, verify: verify_ } = require('../../shims/crypto/index.js') const supportedOpenSSLCiphers = getCiphers() const { Ber } = require('asn1') const bcrypt_pbkdf = require('bcrypt-pbkdf').pbkdf const { CIPHER_INFO } = require('./crypto.js') const { eddsaSupported, SUPPORTED_CIPHER } = require('./constants.js') const { bufferSlice, makeBufferParser, readString, readUInt32BE, writeUInt32BE } = require('./utils.js') const SYM_HASH_ALGO = Symbol('Hash Algorithm') const SYM_PRIV_PEM = Symbol('Private key PEM') const SYM_PUB_PEM = Symbol('Public key PEM') const SYM_PUB_SSH = Symbol('Public key SSH') const SYM_DECRYPTED = Symbol('Decrypted Key') // Create OpenSSL cipher name -> SSH cipher name conversion table const CIPHER_INFO_OPENSSL = Object.create(null) { const keys = Object.keys(CIPHER_INFO) for (let i = 0; i < keys.length; ++i) { const cipherName = CIPHER_INFO[keys[i]].sslName if (!cipherName || CIPHER_INFO_OPENSSL[cipherName]) continue CIPHER_INFO_OPENSSL[cipherName] = CIPHER_INFO[keys[i]] } } const binaryKeyParser = makeBufferParser() function makePEM(type, data) { data = Buffer.from(data).toString('base64') let formatted = data.replace(/.{64}/g, '$&\n') if (data.length & 63) formatted += '\n' return `-----BEGIN ${type} KEY-----\n${formatted}-----END ${type} KEY-----` } function combineBuffers(buf1, buf2) { const result = Buffer.allocUnsafe(buf1.length + buf2.length) result.set(buf1, 0) result.set(buf2, buf1.length) return result } function skipFields(buf, nfields) { const bufLen = buf.length let pos = buf._pos || 0 for (let i = 0; i < nfields; ++i) { const left = bufLen - pos if (pos >= bufLen || left < 4) return false const len = readUInt32BE(buf, pos) if (left < 4 + len) return false pos += 4 + len } buf._pos = pos return true } function genOpenSSLRSAPub(n, e) { const asnWriter = new Ber.Writer() asnWriter.startSequence() // algorithm asnWriter.startSequence() asnWriter.writeOID('1.2.840.113549.1.1.1') // rsaEncryption // algorithm parameters (RSA has none) asnWriter.writeNull() asnWriter.endSequence() // subjectPublicKey asnWriter.startSequence(Ber.BitString) asnWriter.writeByte(0x00) asnWriter.startSequence() asnWriter.writeBuffer(n, Ber.Integer) asnWriter.writeBuffer(e, Ber.Integer) asnWriter.endSequence() asnWriter.endSequence() asnWriter.endSequence() return makePEM('PUBLIC', asnWriter.buffer) } function genOpenSSHRSAPub(n, e) { const publicKey = Buffer.allocUnsafe(4 + 7 + 4 + e.length + 4 + n.length) writeUInt32BE(publicKey, 7, 0) publicKey.utf8Write('ssh-rsa', 4, 7) let i = 4 + 7 writeUInt32BE(publicKey, e.length, i) publicKey.set(e, (i += 4)) writeUInt32BE(publicKey, n.length, (i += e.length)) publicKey.set(n, i + 4) return publicKey } const genOpenSSLRSAPriv = (() => { function genRSAASN1Buf(n, e, d, p, q, dmp1, dmq1, iqmp) { const asnWriter = new Ber.Writer() asnWriter.startSequence() asnWriter.writeInt(0x00, Ber.Integer) asnWriter.writeBuffer(n, Ber.Integer) asnWriter.writeBuffer(e, Ber.Integer) asnWriter.writeBuffer(d, Ber.Integer) asnWriter.writeBuffer(p, Ber.Integer) asnWriter.writeBuffer(q, Ber.Integer) asnWriter.writeBuffer(dmp1, Ber.Integer) asnWriter.writeBuffer(dmq1, Ber.Integer) asnWriter.writeBuffer(iqmp, Ber.Integer) asnWriter.endSequence() return asnWriter.buffer } function bigIntFromBuffer(buf) { return BigInt(`0x${buf.hexSlice(0, buf.length)}`) } function bigIntToBuffer(bn) { let hex = bn.toString(16) if ((hex.length & 1) !== 0) { hex = `0${hex}` } else { const sigbit = hex.charCodeAt(0) // BER/DER integers require leading zero byte to denote a positive value // when first byte >= 0x80 if ( sigbit === 56 /* '8' */ || sigbit === 57 /* '9' */ || (sigbit >= 97 /* 'a' */ && sigbit <= 102) /* 'f' */ ) { hex = `00${hex}` } } return Buffer.from(hex, 'hex') } return function genOpenSSLRSAPriv(n, e, d, iqmp, p, q) { const bn_d = bigIntFromBuffer(d) const dmp1 = bigIntToBuffer(bn_d % (bigIntFromBuffer(p) - 1n)) const dmq1 = bigIntToBuffer(bn_d % (bigIntFromBuffer(q) - 1n)) return makePEM('RSA PRIVATE', genRSAASN1Buf(n, e, d, p, q, dmp1, dmq1, iqmp)) } })() function genOpenSSLDSAPub(p, q, g, y) { const asnWriter = new Ber.Writer() asnWriter.startSequence() // algorithm asnWriter.startSequence() asnWriter.writeOID('1.2.840.10040.4.1') // id-dsa // algorithm parameters asnWriter.startSequence() asnWriter.writeBuffer(p, Ber.Integer) asnWriter.writeBuffer(q, Ber.Integer) asnWriter.writeBuffer(g, Ber.Integer) asnWriter.endSequence() asnWriter.endSequence() // subjectPublicKey asnWriter.startSequence(Ber.BitString) asnWriter.writeByte(0x00) asnWriter.writeBuffer(y, Ber.Integer) asnWriter.endSequence() asnWriter.endSequence() return makePEM('PUBLIC', asnWriter.buffer) } function genOpenSSHDSAPub(p, q, g, y) { const publicKey = Buffer.allocUnsafe( 4 + 7 + 4 + p.length + 4 + q.length + 4 + g.length + 4 + y.length ) writeUInt32BE(publicKey, 7, 0) publicKey.utf8Write('ssh-dss', 4, 7) let i = 4 + 7 writeUInt32BE(publicKey, p.length, i) publicKey.set(p, (i += 4)) writeUInt32BE(publicKey, q.length, (i += p.length)) publicKey.set(q, (i += 4)) writeUInt32BE(publicKey, g.length, (i += q.length)) publicKey.set(g, (i += 4)) writeUInt32BE(publicKey, y.length, (i += g.length)) publicKey.set(y, i + 4) return publicKey } function genOpenSSLDSAPriv(p, q, g, y, x) { const asnWriter = new Ber.Writer() asnWriter.startSequence() asnWriter.writeInt(0x00, Ber.Integer) asnWriter.writeBuffer(p, Ber.Integer) asnWriter.writeBuffer(q, Ber.Integer) asnWriter.writeBuffer(g, Ber.Integer) asnWriter.writeBuffer(y, Ber.Integer) asnWriter.writeBuffer(x, Ber.Integer) asnWriter.endSequence() return makePEM('DSA PRIVATE', asnWriter.buffer) } function genOpenSSLEdPub(pub) { const asnWriter = new Ber.Writer() asnWriter.startSequence() // algorithm asnWriter.startSequence() asnWriter.writeOID('1.3.101.112') // id-Ed25519 asnWriter.endSequence() // PublicKey asnWriter.startSequence(Ber.BitString) asnWriter.writeByte(0x00) // XXX: hack to write a raw buffer without a tag -- yuck asnWriter._ensure(pub.length) asnWriter._buf.set(pub, asnWriter._offset) asnWriter._offset += pub.length asnWriter.endSequence() asnWriter.endSequence() return makePEM('PUBLIC', asnWriter.buffer) } function genOpenSSHEdPub(pub) { const publicKey = Buffer.allocUnsafe(4 + 11 + 4 + pub.length) writeUInt32BE(publicKey, 11, 0) publicKey.utf8Write('ssh-ed25519', 4, 11) writeUInt32BE(publicKey, pub.length, 15) publicKey.set(pub, 19) return publicKey } function genOpenSSLEdPriv(priv) { const asnWriter = new Ber.Writer() asnWriter.startSequence() // version asnWriter.writeInt(0x00, Ber.Integer) // algorithm asnWriter.startSequence() asnWriter.writeOID('1.3.101.112') // id-Ed25519 asnWriter.endSequence() // PrivateKey asnWriter.startSequence(Ber.OctetString) asnWriter.writeBuffer(priv, Ber.OctetString) asnWriter.endSequence() asnWriter.endSequence() return makePEM('PRIVATE', asnWriter.buffer) } function genOpenSSLECDSAPub(oid, Q) { const asnWriter = new Ber.Writer() asnWriter.startSequence() // algorithm asnWriter.startSequence() asnWriter.writeOID('1.2.840.10045.2.1') // id-ecPublicKey // algorithm parameters (namedCurve) asnWriter.writeOID(oid) asnWriter.endSequence() // subjectPublicKey asnWriter.startSequence(Ber.BitString) asnWriter.writeByte(0x00) // XXX: hack to write a raw buffer without a tag -- yuck asnWriter._ensure(Q.length) asnWriter._buf.set(Q, asnWriter._offset) asnWriter._offset += Q.length // end hack asnWriter.endSequence() asnWriter.endSequence() return makePEM('PUBLIC', asnWriter.buffer) } function genOpenSSHECDSAPub(oid, Q) { let curveName switch (oid) { case '1.2.840.10045.3.1.7': // prime256v1/secp256r1 curveName = 'nistp256' break case '1.3.132.0.34': // secp384r1 curveName = 'nistp384' break case '1.3.132.0.35': // secp521r1 curveName = 'nistp521' break default: return } const publicKey = Buffer.allocUnsafe(4 + 19 + 4 + 8 + 4 + Q.length) writeUInt32BE(publicKey, 19, 0) publicKey.utf8Write(`ecdsa-sha2-${curveName}`, 4, 19) writeUInt32BE(publicKey, 8, 23) publicKey.utf8Write(curveName, 27, 8) writeUInt32BE(publicKey, Q.length, 35) publicKey.set(Q, 39) return publicKey } function genOpenSSLECDSAPriv(oid, pub, priv) { const asnWriter = new Ber.Writer() asnWriter.startSequence() // version asnWriter.writeInt(0x01, Ber.Integer) // privateKey asnWriter.writeBuffer(priv, Ber.OctetString) // parameters (optional) asnWriter.startSequence(0xa0) asnWriter.writeOID(oid) asnWriter.endSequence() // publicKey (optional) asnWriter.startSequence(0xa1) asnWriter.startSequence(Ber.BitString) asnWriter.writeByte(0x00) // XXX: hack to write a raw buffer without a tag -- yuck asnWriter._ensure(pub.length) asnWriter._buf.set(pub, asnWriter._offset) asnWriter._offset += pub.length // end hack asnWriter.endSequence() asnWriter.endSequence() asnWriter.endSequence() return makePEM('EC PRIVATE', asnWriter.buffer) } function genOpenSSLECDSAPubFromPriv(curveName, priv) { const tempECDH = createECDH(curveName) tempECDH.setPrivateKey(priv) return tempECDH.getPublicKey() } const BaseKey = { sign: (() => { if (typeof sign_ === 'function') { return function sign(data, algo) { const pem = this[SYM_PRIV_PEM] if (pem === null) return new Error('No private key available') if (!algo || typeof algo !== 'string') algo = this[SYM_HASH_ALGO] try { return sign_(algo, data, pem) } catch (ex) { return ex } } } return function sign(data, algo) { const pem = this[SYM_PRIV_PEM] if (pem === null) return new Error('No private key available') if (!algo || typeof algo !== 'string') algo = this[SYM_HASH_ALGO] const signature = createSign(algo) signature.update(data) try { return signature.sign(pem) } catch (ex) { return ex } } })(), verify: (() => { if (typeof verify_ === 'function') { return function verify(data, signature, algo) { const pem = this[SYM_PUB_PEM] if (pem === null) return new Error('No public key available') if (!algo || typeof algo !== 'string') algo = this[SYM_HASH_ALGO] try { return verify_(algo, data, pem, signature) } catch (ex) { return ex } } } return function verify(data, signature, algo) { const pem = this[SYM_PUB_PEM] if (pem === null) return new Error('No public key available') if (!algo || typeof algo !== 'string') algo = this[SYM_HASH_ALGO] const verifier = createVerify(algo) verifier.update(data) try { return verifier.verify(pem, signature) } catch (ex) { return ex } } })(), isPrivateKey: function isPrivateKey() { return this[SYM_PRIV_PEM] !== null }, getPrivatePEM: function getPrivatePEM() { return this[SYM_PRIV_PEM] }, getPublicPEM: function getPublicPEM() { return this[SYM_PUB_PEM] }, getPublicSSH: function getPublicSSH() { return this[SYM_PUB_SSH] }, equals: function equals(key) { const parsed = parseKey(key) if (parsed instanceof Error) return false return ( this.type === parsed.type && this[SYM_PRIV_PEM] === parsed[SYM_PRIV_PEM] && this[SYM_PUB_PEM] === parsed[SYM_PUB_PEM] && this[SYM_PUB_SSH].equals(parsed[SYM_PUB_SSH]) ) } } function OpenSSH_Private(type, comment, privPEM, pubPEM, pubSSH, algo, decrypted) { this.type = type this.comment = comment this[SYM_PRIV_PEM] = privPEM this[SYM_PUB_PEM] = pubPEM this[SYM_PUB_SSH] = pubSSH this[SYM_HASH_ALGO] = algo this[SYM_DECRYPTED] = decrypted } OpenSSH_Private.prototype = BaseKey { const regexp = /^-----BEGIN OPENSSH PRIVATE KEY-----(?:\r\n|\n)([\s\S]+)(?:\r\n|\n)-----END OPENSSH PRIVATE KEY-----$/ OpenSSH_Private.parse = (str, passphrase) => { const m = regexp.exec(str) if (m === null) return null let ret const data = Buffer.from(m[1].replace(/\s+/g, ''), 'base64') if (data.length < 31) // magic (+ magic null term.) + minimum field lengths return new Error('Malformed OpenSSH private key') const magic = data.toString('utf8', 0, 15) if (magic !== 'openssh-key-v1\0') return new Error(`Unsupported OpenSSH key magic: ${magic}`) const cipherName = readString(data, 15, true) if (cipherName === undefined) return new Error('Malformed OpenSSH private key') if (cipherName !== 'none' && SUPPORTED_CIPHER.indexOf(cipherName) === -1) return new Error(`Unsupported cipher for OpenSSH key: ${cipherName}`) const kdfName = readString(data, data._pos, true) if (kdfName === undefined) return new Error('Malformed OpenSSH private key') if (kdfName !== 'none') { if (cipherName === 'none') return new Error('Malformed OpenSSH private key') if (kdfName !== 'bcrypt') return new Error(`Unsupported kdf name for OpenSSH key: ${kdfName}`) if (!passphrase) { return new Error('Encrypted private OpenSSH key detected, but no passphrase given') } } else if (cipherName !== 'none') { return new Error('Malformed OpenSSH private key') } let encInfo let cipherKey let cipherIV if (cipherName !== 'none') encInfo = CIPHER_INFO[cipherName] const kdfOptions = readString(data, data._pos) if (kdfOptions === undefined) return new Error('Malformed OpenSSH private key') if (kdfOptions.length) { switch (kdfName) { case 'none': return new Error('Malformed OpenSSH private key') case 'bcrypt': { /* string salt uint32 rounds */ const salt = readString(kdfOptions, 0) if (salt === undefined || kdfOptions._pos + 4 > kdfOptions.length) return new Error('Malformed OpenSSH private key') const rounds = readUInt32BE(kdfOptions, kdfOptions._pos) const gen = Buffer.allocUnsafe(encInfo.keyLen + encInfo.ivLen) const r = bcrypt_pbkdf( passphrase, passphrase.length, salt, salt.length, gen, gen.length, rounds ) if (r !== 0) return new Error('Failed to generate information to decrypt key') cipherKey = bufferSlice(gen, 0, encInfo.keyLen) cipherIV = bufferSlice(gen, encInfo.keyLen, gen.length) break } } } else if (kdfName !== 'none') { return new Error('Malformed OpenSSH private key') } if (data._pos + 3 >= data.length) return new Error('Malformed OpenSSH private key') const keyCount = readUInt32BE(data, data._pos) data._pos += 4 if (keyCount > 0) { // TODO: place sensible limit on max `keyCount` // Read public keys first for (let i = 0; i < keyCount; ++i) { const pubData = readString(data, data._pos) if (pubData === undefined) return new Error('Malformed OpenSSH private key') const type = readString(pubData, 0, true) if (type === undefined) return new Error('Malformed OpenSSH private key') } let privBlob = readString(data, data._pos) if (privBlob === undefined) return new Error('Malformed OpenSSH private key') if (cipherKey !== undefined) { // Encrypted private key(s) if (privBlob.length < encInfo.blockLen || privBlob.length % encInfo.blockLen !== 0) { return new Error('Malformed OpenSSH private key') } try { const options = { authTagLength: encInfo.authLen } const decipher = createDecipheriv(encInfo.sslName, cipherKey, cipherIV, options) try { decipher.setAutoPadding(false) } catch { /* bare-crypto AEAD */ } if (encInfo.authLen > 0) { if (data.length - data._pos < encInfo.authLen) return new Error('Malformed OpenSSH private key') decipher.setAuthTag(bufferSlice(data, data._pos, (data._pos += encInfo.authLen))) } privBlob = combineBuffers(decipher.update(privBlob), decipher.final()) } catch (ex) { return ex } } // Nothing should we follow the private key(s), except a possible // authentication tag for relevant ciphers if (data._pos !== data.length) return new Error('Malformed OpenSSH private key') ret = parseOpenSSHPrivKeys(privBlob, keyCount, cipherKey !== undefined) } else { ret = [] } if (ret instanceof Error) return ret // This will need to change if/when OpenSSH ever starts storing multiple // keys in their key files return ret[0] } function parseOpenSSHPrivKeys(data, nkeys, decrypted) { const keys = [] /* uint32 checkint uint32 checkint string privatekey1 string comment1 string privatekey2 string comment2 ... string privatekeyN string commentN char 1 char 2 char 3 ... char padlen % 255 */ if (data.length < 8) return new Error('Malformed OpenSSH private key') const check1 = readUInt32BE(data, 0) const check2 = readUInt32BE(data, 4) if (check1 !== check2) { if (decrypted) { return new Error('OpenSSH key integrity check failed -- bad passphrase?') } return new Error('OpenSSH key integrity check failed') } data._pos = 8 let i let oid for (i = 0; i < nkeys; ++i) { let algo let privPEM let pubPEM let pubSSH // The OpenSSH documentation for the key format actually lies, the // entirety of the private key content is not contained with a string // field, it's actually the literal contents of the private key, so to be // able to find the end of the key data you need to know the layout/format // of each key type ... const type = readString(data, data._pos, true) if (type === undefined) return new Error('Malformed OpenSSH private key') switch (type) { case 'ssh-rsa': { /* string n -- public string e -- public string d -- private string iqmp -- private string p -- private string q -- private */ const n = readString(data, data._pos) if (n === undefined) return new Error('Malformed OpenSSH private key') const e = readString(data, data._pos) if (e === undefined) return new Error('Malformed OpenSSH private key') const d = readString(data, data._pos) if (d === undefined) return new Error('Malformed OpenSSH private key') const iqmp = readString(data, data._pos) if (iqmp === undefined) return new Error('Malformed OpenSSH private key') const p = readString(data, data._pos) if (p === undefined) return new Error('Malformed OpenSSH private key') const q = readString(data, data._pos) if (q === undefined) return new Error('Malformed OpenSSH private key') pubPEM = genOpenSSLRSAPub(n, e) pubSSH = genOpenSSHRSAPub(n, e) privPEM = genOpenSSLRSAPriv(n, e, d, iqmp, p, q) algo = 'sha1' break } case 'ssh-dss': { /* string p -- public string q -- public string g -- public string y -- public string x -- private */ const p = readString(data, data._pos) if (p === undefined) return new Error('Malformed OpenSSH private key') const q = readString(data, data._pos) if (q === undefined) return new Error('Malformed OpenSSH private key') const g = readString(data, data._pos) if (g === undefined) return new Error('Malformed OpenSSH private key') const y = readString(data, data._pos) if (y === undefined) return new Error('Malformed OpenSSH private key') const x = readString(data, data._pos) if (x === undefined) return new Error('Malformed OpenSSH private key') pubPEM = genOpenSSLDSAPub(p, q, g, y) pubSSH = genOpenSSHDSAPub(p, q, g, y) privPEM = genOpenSSLDSAPriv(p, q, g, y, x) algo = 'sha1' break } case 'ssh-ed25519': { if (!eddsaSupported) return new Error(`Unsupported OpenSSH private key type: ${type}`) /* * string public key * string private key + public key */ const edpub = readString(data, data._pos) if (edpub === undefined || edpub.length !== 32) return new Error('Malformed OpenSSH private key') const edpriv = readString(data, data._pos) if (edpriv === undefined || edpriv.length !== 64) return new Error('Malformed OpenSSH private key') pubPEM = genOpenSSLEdPub(edpub) pubSSH = genOpenSSHEdPub(edpub) privPEM = genOpenSSLEdPriv(bufferSlice(edpriv, 0, 32)) algo = null break } case 'ecdsa-sha2-nistp256': algo = 'sha256' oid = '1.2.840.10045.3.1.7' // FALLTHROUGH case 'ecdsa-sha2-nistp384': if (algo === undefined) { algo = 'sha384' oid = '1.3.132.0.34' } // FALLTHROUGH case 'ecdsa-sha2-nistp521': { if (algo === undefined) { algo = 'sha512' oid = '1.3.132.0.35' } /* string curve name string Q -- public string d -- private */ // TODO: validate curve name against type if (!skipFields(data, 1)) // Skip curve name return new Error('Malformed OpenSSH private key') const ecpub = readString(data, data._pos) if (ecpub === undefined) return new Error('Malformed OpenSSH private key') const ecpriv = readString(data, data._pos) if (ecpriv === undefined) return new Error('Malformed OpenSSH private key') pubPEM = genOpenSSLECDSAPub(oid, ecpub) pubSSH = genOpenSSHECDSAPub(oid, ecpub) privPEM = genOpenSSLECDSAPriv(oid, ecpub, ecpriv) break } default: return new Error(`Unsupported OpenSSH private key type: ${type}`) } const privComment = readString(data, data._pos, true) if (privComment === undefined) return new Error('Malformed OpenSSH private key') keys.push(new OpenSSH_Private(type, privComment, privPEM, pubPEM, pubSSH, algo, decrypted)) } let cnt = 0 for (i = data._pos; i < data.length; ++i) { if (data[i] !== ++cnt % 255) return new Error('Malformed OpenSSH private key') } return keys } } function OpenSSH_Old_Private(type, comment, privPEM, pubPEM, pubSSH, algo, decrypted) { this.type = type this.comment = comment this[SYM_PRIV_PEM] = privPEM this[SYM_PUB_PEM] = pubPEM this[SYM_PUB_SSH] = pubSSH this[SYM_HASH_ALGO] = algo this[SYM_DECRYPTED] = decrypted } OpenSSH_Old_Private.prototype = BaseKey { const regexp = /^-----BEGIN (RSA|DSA|EC) PRIVATE KEY-----(?:\r\n|\n)((?:[^:]+:\s*[\S].*(?:\r\n|\n))*)([\s\S]+)(?:\r\n|\n)-----END (RSA|DSA|EC) PRIVATE KEY-----$/ OpenSSH_Old_Private.parse = (str, passphrase) => { const m = regexp.exec(str) if (m === null) return null let privBlob = Buffer.from(m[3].replace(/\s+/g, ''), 'base64') let headers = m[2] let decrypted = false if (headers !== undefined) { // encrypted key headers = headers.split(/\r\n|\n/g) for (let i = 0; i < headers.length; ++i) { const header = headers[i] let sepIdx = header.indexOf(':') if (header.slice(0, sepIdx) === 'DEK-Info') { const val = header.slice(sepIdx + 2) sepIdx = val.indexOf(',') if (sepIdx === -1) continue const cipherName = val.slice(0, sepIdx).toLowerCase() if (supportedOpenSSLCiphers.indexOf(cipherName) === -1) { return new Error( `Cipher (${cipherName}) not supported ` + 'for encrypted OpenSSH private key' ) } const encInfo = CIPHER_INFO_OPENSSL[cipherName] if (!encInfo) { return new Error( `Cipher (${cipherName}) not supported ` + 'for encrypted OpenSSH private key' ) } const cipherIV = Buffer.from(val.slice(sepIdx + 1), 'hex') if (cipherIV.length !== encInfo.ivLen) return new Error('Malformed encrypted OpenSSH private key') if (!passphrase) { return new Error('Encrypted OpenSSH private key detected, but no passphrase given') } const ivSlice = bufferSlice(cipherIV, 0, 8) let cipherKey = createHash('md5').update(passphrase).update(ivSlice).digest() while (cipherKey.length < encInfo.keyLen) { cipherKey = combineBuffers( cipherKey, createHash('md5').update(cipherKey).update(passphrase).update(ivSlice).digest() ) } if (cipherKey.length > encInfo.keyLen) cipherKey = bufferSlice(cipherKey, 0, encInfo.keyLen) try { const decipher = createDecipheriv(cipherName, cipherKey, cipherIV) try { decipher.setAutoPadding(false) } catch { /* bare-crypto AEAD */ } privBlob = combineBuffers(decipher.update(privBlob), decipher.final()) decrypted = true } catch (ex) { return ex } } } } let type let privPEM let pubPEM let pubSSH let algo let reader let errMsg = 'Malformed OpenSSH private key' if (decrypted) errMsg += '. Bad passphrase?' switch (m[1]) { case 'RSA': type = 'ssh-rsa' privPEM = makePEM('RSA PRIVATE', privBlob) try { reader = new Ber.Reader(privBlob) reader.readSequence() reader.readInt() // skip version const n = reader.readString(Ber.Integer, true) if (n === null) return new Error(errMsg) const e = reader.readString(Ber.Integer, true) if (e === null) return new Error(errMsg) pubPEM = genOpenSSLRSAPub(n, e) pubSSH = genOpenSSHRSAPub(n, e) } catch { return new Error(errMsg) } algo = 'sha1' break case 'DSA': type = 'ssh-dss' privPEM = makePEM('DSA PRIVATE', privBlob) try { reader = new Ber.Reader(privBlob) reader.readSequence() reader.readInt() // skip version const p = reader.readString(Ber.Integer, true) if (p === null) return new Error(errMsg) const q = reader.readString(Ber.Integer, true) if (q === null) return new Error(errMsg) const g = reader.readString(Ber.Integer, true) if (g === null) return new Error(errMsg) const y = reader.readString(Ber.Integer, true) if (y === null) return new Error(errMsg) pubPEM = genOpenSSLDSAPub(p, q, g, y) pubSSH = genOpenSSHDSAPub(p, q, g, y) } catch { return new Error(errMsg) } algo = 'sha1' break case 'EC': { let ecSSLName let ecPriv let ecOID try { reader = new Ber.Reader(privBlob) reader.readSequence() reader.readInt() // skip version ecPriv = reader.readString(Ber.OctetString, true) reader.readByte() // Skip "complex" context type byte const offset = reader.readLength() // Skip context length if (offset !== null) { reader._offset = offset ecOID = reader.readOID() if (ecOID === null) return new Error(errMsg) switch (ecOID) { case '1.2.840.10045.3.1.7': // prime256v1/secp256r1 ecSSLName = 'prime256v1' type = 'ecdsa-sha2-nistp256' algo = 'sha256' break case '1.3.132.0.34': // secp384r1 ecSSLName = 'secp384r1' type = 'ecdsa-sha2-nistp384' algo = 'sha384' break case '1.3.132.0.35': // secp521r1 ecSSLName = 'secp521r1' type = 'ecdsa-sha2-nistp521' algo = 'sha512' break default: return new Error(`Unsupported private key EC OID: ${ecOID}`) } } else { return new Error(errMsg) } } catch { return new Error(errMsg) } privPEM = makePEM('EC PRIVATE', privBlob) const pubBlob = genOpenSSLECDSAPubFromPriv(ecSSLName, ecPriv) pubPEM = genOpenSSLECDSAPub(ecOID, pubBlob) pubSSH = genOpenSSHECDSAPub(ecOID, pubBlob) break } } return new OpenSSH_Old_Private(type, '', privPEM, pubPEM, pubSSH, algo, decrypted) } } function PPK_Private(type, comment, privPEM, pubPEM, pubSSH, algo, decrypted) { this.type = type this.comment = comment this[SYM_PRIV_PEM] = privPEM this[SYM_PUB_PEM] = pubPEM this[SYM_PUB_SSH] = pubSSH this[SYM_HASH_ALGO] = algo this[SYM_DECRYPTED] = decrypted } PPK_Private.prototype = BaseKey { const EMPTY_PASSPHRASE = Buffer.alloc(0) const PPK_IV = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) const PPK_PP1 = Buffer.from([0, 0, 0, 0]) const PPK_PP2 = Buffer.from([0, 0, 0, 1]) const regexp = /^PuTTY-User-Key-File-2: (ssh-(?:rsa|dss))\r?\nEncryption: (aes256-cbc|none)\r?\nComment: ([^\r\n]*)\r?\nPublic-Lines: \d+\r?\n([\s\S]+?)\r?\nPrivate-Lines: \d+\r?\n([\s\S]+?)\r?\nPrivate-MAC: ([^\r\n]+)/ PPK_Private.parse = (str, passphrase) => { const m = regexp.exec(str) if (m === null) return null // m[1] = key type // m[2] = encryption type // m[3] = comment // m[4] = base64-encoded public key data: // for "ssh-rsa": // string "ssh-rsa" // mpint e (public exponent) // mpint n (modulus) // for "ssh-dss": // string "ssh-dss" // mpint p (modulus) // mpint q (prime) // mpint g (base number) // mpint y (public key parameter: g^x mod p) // m[5] = base64-encoded private key data: // for "ssh-rsa": // mpint d (private exponent) // mpint p (prime 1) // mpint q (prime 2) // mpint iqmp ([inverse of q] mod p) // for "ssh-dss": // mpint x (private key parameter) // m[6] = SHA1 HMAC over: // string name of algorithm ("ssh-dss", "ssh-rsa") // string encryption type // string comment // string public key data // string private-plaintext (including the final padding) const cipherName = m[2] const encrypted = cipherName !== 'none' if (encrypted && !passphrase) { return new Error('Encrypted PPK private key detected, but no passphrase given') } let privBlob = Buffer.from(m[5].replace(/\s+/g, ''), 'base64') if (encrypted) { const encInfo = CIPHER_INFO[cipherName] let cipherKey = combineBuffers( createHash('sha1').update(PPK_PP1).update(passphrase).digest(), createHash('sha1').update(PPK_PP2).update(passphrase).digest() ) if (cipherKey.length > encInfo.keyLen) cipherKey = bufferSlice(cipherKey, 0, encInfo.keyLen) try { const decipher = createDecipheriv(encInfo.sslName, cipherKey, PPK_IV) try { decipher.setAutoPadding(false) } catch { /* bare-crypto AEAD */ } privBlob = combineBuffers(decipher.update(privBlob), decipher.final()) } catch (ex) { return ex } } const type = m[1] const comment = m[3] const pubBlob = Buffer.from(m[4].replace(/\s+/g, ''), 'base64') const mac = m[6] const typeLen = type.length const cipherNameLen = cipherName.length const commentLen = Buffer.byteLength(comment) const pubLen = pubBlob.length const privLen = privBlob.length const macData = Buffer.allocUnsafe( 4 + typeLen + 4 + cipherNameLen + 4 + commentLen + 4 + pubLen + 4 + privLen ) let p = 0 writeUInt32BE(macData, typeLen, p) macData.utf8Write(type, (p += 4), typeLen) writeUInt32BE(macData, cipherNameLen, (p += typeLen)) macData.utf8Write(cipherName, (p += 4), cipherNameLen) writeUInt32BE(macData, commentLen, (p += cipherNameLen)) macData.utf8Write(comment, (p += 4), commentLen) writeUInt32BE(macData, pubLen, (p += commentLen)) macData.set(pubBlob, (p += 4)) writeUInt32BE(macData, privLen, (p += pubLen)) macData.set(privBlob, p + 4) if (!passphrase) passphrase = EMPTY_PASSPHRASE const calcMAC = createHmac( 'sha1', createHash('sha1').update('putty-private-key-file-mac-key').update(passphrase).digest() ) .update(macData) .digest('hex') if (calcMAC !== mac) { if (encrypted) { return new Error('PPK private key integrity check failed -- bad passphrase?') } return new Error('PPK private key integrity check failed') } let pubPEM let pubSSH let privPEM pubBlob._pos = 0 skipFields(pubBlob, 1) // skip (duplicate) key type switch (type) { case 'ssh-rsa': { const e = readString(pubBlob, pubBlob._pos) if (e === undefined) return new Error('Malformed PPK public key') const n = readString(pubBlob, pubBlob._pos) if (n === undefined) return new Error('Malformed PPK public key') const d = readString(privBlob, 0) if (d === undefined) return new Error('Malformed PPK private key') const p = readString(privBlob, privBlob._pos) if (p === undefined) return new Error('Malformed PPK private key') const q = readString(privBlob, privBlob._pos) if (q === undefined) return new Error('Malformed PPK private key') const iqmp = readString(privBlob, privBlob._pos) if (iqmp === undefined) return new Error('Malformed PPK private key') pubPEM = genOpenSSLRSAPub(n, e) pubSSH = genOpenSSHRSAPub(n, e) privPEM = genOpenSSLRSAPriv(n, e, d, iqmp, p, q) break } case 'ssh-dss': { const p = readString(pubBlob, pubBlob._pos) if (p === undefined) return new Error('Malformed PPK public key') const q = readString(pubBlob, pubBlob._pos) if (q === undefined) return new Error('Malformed PPK public key') const g = readString(pubBlob, pubBlob._pos) if (g === undefined) return new Error('Malformed PPK public key') const y = readString(pubBlob, pubBlob._pos) if (y === undefined) return new Error('Malformed PPK public key') const x = readString(privBlob, 0) if (x === undefined) return new Error('Malformed PPK private key') pubPEM = genOpenSSLDSAPub(p, q, g, y) pubSSH = genOpenSSHDSAPub(p, q, g, y) privPEM = genOpenSSLDSAPriv(p, q, g, y, x) break } } return new PPK_Private(type, comment, privPEM, pubPEM, pubSSH, 'sha1', encrypted) } } function OpenSSH_Public(type, comment, pubPEM, pubSSH, algo) { this.type = type this.comment = comment this[SYM_PRIV_PEM] = null this[SYM_PUB_PEM] = pubPEM this[SYM_PUB_SSH] = pubSSH this[SYM_HASH_ALGO] = algo this[SYM_DECRYPTED] = false } OpenSSH_Public.prototype = BaseKey { let regexp if (eddsaSupported) regexp = /^(((?:ssh-(?:rsa|dss|ed25519))|ecdsa-sha2-nistp(?:256|384|521))(?:-cert-v0[01]@openssh.com)?) ([A-Z0-9a-z/+=]+)(?:$|\s+([\S].*)?)$/ else regexp = /^(((?:ssh-(?:rsa|dss))|ecdsa-sha2-nistp(?:256|384|521))(?:-cert-v0[01]@openssh.com)?) ([A-Z0-9a-z/+=]+)(?:$|\s+([\S].*)?)$/ OpenSSH_Public.parse = (str) => { const m = regexp.exec(str) if (m === null) return null // m[1] = full type // m[2] = base type // m[3] = base64-encoded public key // m[4] = comment const fullType = m[1] const baseType = m[2] const data = Buffer.from(m[3].replace(/\s+/g, ''), 'base64') const comment = m[4] || '' const type = readString(data, data._pos, true) if (type === undefined || type.indexOf(baseType) !== 0) return new Error('Malformed OpenSSH public key') return parseDER(data, baseType, comment, fullType) } } function RFC4716_Public(type, comment, pubPEM, pubSSH, algo) { this.type = type this.comment = comment this[SYM_PRIV_PEM] = null this[SYM_PUB_PEM] = pubPEM this[SYM_PUB_SSH] = pubSSH this[SYM_HASH_ALGO] = algo this[SYM_DECRYPTED] = false } RFC4716_Public.prototype = BaseKey { const regexp = /^---- BEGIN SSH2 PUBLIC KEY ----(?:\r?\n)((?:.{0,72}\r?\n)+)---- END SSH2 PUBLIC KEY ----$/ const RE_DATA = /^[A-Z0-9a-z/+=\r\n]+$/ const RE_HEADER = /^([\x21-\x39\x3B-\x7E]{1,64}): ((?:[^\\]*\\\r?\n)*[^\r\n]+)\r?\n/gm const RE_HEADER_ENDS = /\\\r?\n/g RFC4716_Public.parse = (str) => { let m = regexp.exec(str) if (m === null) return null const body = m[1] let dataStart = 0 let comment = '' while ((m = RE_HEADER.exec(body))) { const headerName = m[1] const headerValue = m[2].replace(RE_HEADER_ENDS, '') if (headerValue.length > 1024) { RE_HEADER.lastIndex = 0 return new Error('Malformed RFC4716 public key') } dataStart = RE_HEADER.lastIndex if (headerName.toLowerCase() === 'comment') { comment = headerValue if ( comment.length > 1 && comment.charCodeAt(0) === 34 /* '"' */ && comment.charCodeAt(comment.length - 1) === 34 /* '"' */ ) { comment = comment.slice(1, -1) } } } let data = body.slice(dataStart) if (!RE_DATA.test(data)) return new Error('Malformed RFC4716 public key') data = Buffer.from(data, 'base64') const type = readString(data, 0, true) if (type === undefined) return new Error('Malformed RFC4716 public key') let pubPEM = null let pubSSH = null switch (type) { case 'ssh-rsa': { const e = readString(data, data._pos) if (e === undefined) return new Error('Malformed RFC4716 public key') const n = readString(data, data._pos) if (n === undefined) return new Error('Malformed RFC4716 public key') pubPEM = genOpenSSLRSAPub(n, e) pubSSH = genOpenSSHRSAPub(n, e) break } case 'ssh-dss': { const p = readString(data, data._pos) if (p === undefined) return new Error('Malformed RFC4716 public key') const q = readString(data, data._pos) if (q === undefined) return new Error('Malformed RFC4716 public key') const g = readString(data, data._pos) if (g === undefined) return new Error('Malformed RFC4716 public key') const y = readString(data, data._pos) if (y === undefined) return new Error('Malformed RFC4716 public key') pubPEM = genOpenSSLDSAPub(p, q, g, y) pubSSH = genOpenSSHDSAPub(p, q, g, y) break } default: return new Error('Malformed RFC4716 public key') } return new RFC4716_Public(type, comment, pubPEM, pubSSH, 'sha1') } } function parseDER(data, baseType, comment, fullType) { if (!isSupportedKeyType(baseType)) return new Error(`Unsupported OpenSSH public key type: ${baseType}`) let algo let oid let pubPEM = null let pubSSH = null switch (baseType) { case 'ssh-rsa': { const e = readString(data, data._pos || 0) if (e === undefined) return new Error('Malformed OpenSSH public key') const n = readString(data, data._pos) if (n === undefined) return new Error('Malformed OpenSSH public key') pubPEM = genOpenSSLRSAPub(n, e) pubSSH = genOpenSSHRSAPub(n, e) algo = 'sha1' break } case 'ssh-dss': { const p = readString(data, data._pos || 0) if (p === undefined) return new Error('Malformed OpenSSH public key') const q = readString(data, data._pos) if (q === undefined) return new Error('Malformed OpenSSH public key') const g = readString(data, data._pos) if (g === undefined) return new Error('Malformed OpenSSH public key') const y = readString(data, data._pos) if (y === undefined) return new Error('Malformed OpenSSH public key') pubPEM = genOpenSSLDSAPub(p, q, g, y) pubSSH = genOpenSSHDSAPub(p, q, g, y) algo = 'sha1' break } case 'ssh-ed25519': { const edpub = readString(data, data._pos || 0) if (edpub === undefined || edpub.length !== 32) return new Error('Malformed OpenSSH public key') pubPEM = genOpenSSLEdPub(edpub) pubSSH = genOpenSSHEdPub(edpub) algo = null break } case 'ecdsa-sha2-nistp256': algo = 'sha256' oid = '1.2.840.10045.3.1.7' // FALLTHROUGH case 'ecdsa-sha2-nistp384': if (algo === undefined) { algo = 'sha384' oid = '1.3.132.0.34' } // FALLTHROUGH case 'ecdsa-sha2-nistp521': { if (algo === undefined) { algo = 'sha512' oid = '1.3.132.0.35' } // TODO: validate curve name against type if (!skipFields(data, 1)) // Skip curve name return new Error('Malformed OpenSSH public key') const ecpub = readString(data, data._pos || 0) if (ecpub === undefined) return new Error('Malformed OpenSSH public key') pubPEM = genOpenSSLECDSAPub(oid, ecpub) pubSSH = genOpenSSHECDSAPub(oid, ecpub) break } default: return new Error(`Unsupported OpenSSH public key type: ${baseType}`) } return new OpenSSH_Public(fullType, comment, pubPEM, pubSSH, algo) } function isSupportedKeyType(type) { switch (type) { case 'ssh-rsa': case 'ssh-dss': case 'ecdsa-sha2-nistp256': case 'ecdsa-sha2-nistp384': case 'ecdsa-sha2-nistp521': return true case 'ssh-ed25519': if (eddsaSupported) return true // FALLTHROUGH default: return false } } function isParsedKey(val) { if (!val) return false return typeof val[SYM_DECRYPTED] === 'boolean' } function parseKey(data, passphrase) { if (isParsedKey(data)) return data let origBuffer if (Buffer.isBuffer(data)) { origBuffer = data data = data.toString('utf8').trim() } else if (typeof data === 'string') { data = data.trim() } else { return new Error('Key data must be a Buffer or string') } // eslint-disable-next-line eqeqeq if (passphrase != undefined) { if (typeof passphrase === 'string') passphrase = Buffer.from(passphrase) else if (!Buffer.isBuffer(passphrase)) return new Error('Passphrase must be a string or Buffer when supplied') } let ret // First try as printable string format (e.g. PEM) // Private keys if ((ret = OpenSSH_Private.parse(data, passphrase)) !== null) return ret if ((ret = OpenSSH_Old_Private.parse(data, passphrase)) !== null) return ret if ((ret = PPK_Private.parse(data, passphrase)) !== null) return ret // Public keys if ((ret = OpenSSH_Public.parse(data)) !== null) return ret if ((ret = RFC4716_Public.parse(data)) !== null) return ret // Finally try as a binary format if we were originally passed binary data if (origBuffer) { binaryKeyParser.init(origBuffer, 0) const type = binaryKeyParser.readString(true) if (type !== undefined) { data = binaryKeyParser.readRaw() if (data !== undefined) { ret = parseDER(data, type, '', type) // Ignore potentially useless errors in case the data was not actually // in the binary format if (ret instanceof Error) ret = null } } binaryKeyParser.clear() } if (ret) return ret return new Error('Unsupported key format') } module.exports = { isParsedKey, isSupportedKeyType, parseDERKey: (data, type) => parseDER(data, type, '', type), parseKey }