// TODO: // * utilize `crypto.create(Private|Public)Key()` and `keyObject.export()` // * handle multi-line header values (OpenSSH)? // * more thorough validation? var crypto = require('crypto'); var cryptoSign = crypto.sign; var cryptoVerify = crypto.verify; var createSign = crypto.createSign; var createVerify = crypto.createVerify; var createDecipheriv = crypto.createDecipheriv; var createHash = crypto.createHash; var createHmac = crypto.createHmac; var supportedOpenSSLCiphers = crypto.getCiphers(); var utils; var Ber = require('asn1').Ber; var bcrypt_pbkdf = require('bcrypt-pbkdf').pbkdf; var bufferHelpers = require(__dirname + '/buffer-helpers'); var readUInt32BE = bufferHelpers.readUInt32BE; var writeUInt32BE = bufferHelpers.writeUInt32BE; var constants = require(__dirname + '/constants'); var SUPPORTED_CIPHER = constants.ALGORITHMS.SUPPORTED_CIPHER; var CIPHER_INFO = constants.CIPHER_INFO; var SSH_TO_OPENSSL = constants.SSH_TO_OPENSSL; var EDDSA_SUPPORTED = constants.EDDSA_SUPPORTED; var SYM_HASH_ALGO = Symbol('Hash Algorithm'); var SYM_PRIV_PEM = Symbol('Private key PEM'); var SYM_PUB_PEM = Symbol('Public key PEM'); var SYM_PUB_SSH = Symbol('Public key SSH'); var SYM_DECRYPTED = Symbol('Decrypted Key'); // Create OpenSSL cipher name -> SSH cipher name conversion table var CIPHER_INFO_OPENSSL = Object.create(null); (function() { var keys = Object.keys(CIPHER_INFO); for (var i = 0; i < keys.length; ++i) { var cipherName = SSH_TO_OPENSSL[keys[i]]; if (!cipherName || CIPHER_INFO_OPENSSL[cipherName]) continue; CIPHER_INFO_OPENSSL[cipherName] = CIPHER_INFO[keys[i]]; } })(); var trimStart = (function() { if (typeof String.prototype.trimStart === 'function') { return function trimStart(str) { return str.trimStart(); }; } return function trimStart(str) { var start = 0; for (var i = 0; i < str.length; ++i) { switch (str.charCodeAt(i)) { case 32: // ' ' case 9: // '\t' case 13: // '\r' case 10: // '\n' case 12: // '\f' ++start; continue; } break; } if (start === 0) return str; return str.slice(start); }; })(); function makePEM(type, data) { data = data.toString('base64'); return '-----BEGIN ' + type + ' KEY-----\n' + data.replace(/.{64}/g, '$&\n') + (data.length % 64 ? '\n' : '') + '-----END ' + type + ' KEY-----'; } function combineBuffers(buf1, buf2) { var result = Buffer.allocUnsafe(buf1.length + buf2.length); buf1.copy(result, 0); buf2.copy(result, buf1.length); return result; } function skipFields(buf, nfields) { var bufLen = buf.length; var pos = (buf._pos || 0); for (var i = 0; i < nfields; ++i) { var left = (bufLen - pos); if (pos >= bufLen || left < 4) return false; var len = readUInt32BE(buf, pos); if (left < 4 + len) return false; pos += 4 + len; } buf._pos = pos; return true; } function genOpenSSLRSAPub(n, e) { var 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) { var publicKey = Buffer.allocUnsafe(4 + 7 // "ssh-rsa" + 4 + n.length + 4 + e.length); writeUInt32BE(publicKey, 7, 0); publicKey.write('ssh-rsa', 4, 7, 'ascii'); var i = 4 + 7; writeUInt32BE(publicKey, e.length, i); e.copy(publicKey, i += 4); writeUInt32BE(publicKey, n.length, i += e.length); n.copy(publicKey, i + 4); return publicKey; } var genOpenSSLRSAPriv = (function() { function genRSAASN1Buf(n, e, d, p, q, dmp1, dmq1, iqmp) { var 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.toString('hex')); } function bigIntToBuffer(bn) { var hex = bn.toString(16); if ((hex.length & 1) !== 0) { hex = '0' + hex; } else { var sigbit = hex.charCodeAt(0); // BER/DER integers require leading zero byte to denote a positive value // when first byte >= 0x80 if (sigbit === 56 || (sigbit >= 97 && sigbit <= 102)) hex = '00' + hex; } return Buffer.from(hex, 'hex'); } // Feature detect native BigInt availability and use it when possible try { var code = [ 'return function genOpenSSLRSAPriv(n, e, d, iqmp, p, q) {', ' var bn_d = bigIntFromBuffer(d);', ' var dmp1 = bigIntToBuffer(bn_d % (bigIntFromBuffer(p) - 1n));', ' var dmq1 = bigIntToBuffer(bn_d % (bigIntFromBuffer(q) - 1n));', ' return makePEM(\'RSA PRIVATE\', ' + 'genRSAASN1Buf(n, e, d, p, q, dmp1, dmq1, iqmp));', '};' ].join('\n'); return new Function( 'bigIntFromBuffer, bigIntToBuffer, makePEM, genRSAASN1Buf', code )(bigIntFromBuffer, bigIntToBuffer, makePEM, genRSAASN1Buf); } catch (ex) { return (function() { var BigInteger = require(__dirname + '/jsbn.js'); return function genOpenSSLRSAPriv(n, e, d, iqmp, p, q) { var pbi = new BigInteger(p, 256); var qbi = new BigInteger(q, 256); var dbi = new BigInteger(d, 256); var dmp1bi = dbi.mod(pbi.subtract(BigInteger.ONE)); var dmq1bi = dbi.mod(qbi.subtract(BigInteger.ONE)); var dmp1 = Buffer.from(dmp1bi.toByteArray()); var dmq1 = Buffer.from(dmq1bi.toByteArray()); return makePEM('RSA PRIVATE', genRSAASN1Buf(n, e, d, p, q, dmp1, dmq1, iqmp)); }; })(); } })(); function genOpenSSLDSAPub(p, q, g, y) { var 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) { var publicKey = Buffer.allocUnsafe(4 + 7 // ssh-dss + 4 + p.length + 4 + q.length + 4 + g.length + 4 + y.length); writeUInt32BE(publicKey, 7, 0); publicKey.write('ssh-dss', 4, 7, 'ascii'); var i = 4 + 7; writeUInt32BE(publicKey, p.length, i); p.copy(publicKey, i += 4); writeUInt32BE(publicKey, q.length, i += p.length); q.copy(publicKey, i += 4); writeUInt32BE(publicKey, g.length, i += q.length); g.copy(publicKey, i += 4); writeUInt32BE(publicKey, y.length, i += g.length); y.copy(publicKey, i + 4); return publicKey; } function genOpenSSLDSAPriv(p, q, g, y, x) { var 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) { var asnWriter = new Ber.Writer(); asnWriter.startSequence(); // algorithm asnWriter.startSequence(); asnWriter.writeOID('1.3.101.112'); // id-Ed25519 asnWriter.endSequence(); // PublicKey asnWriter.writeBuffer(pub, Ber.BitString); asnWriter.endSequence(); return makePEM('PUBLIC', asnWriter.buffer); } function genOpenSSHEdPub(pub) { var publicKey = Buffer.allocUnsafe(4 + 11 // ssh-ed25519 + 4 + pub.length); writeUInt32BE(publicKey, 11, 0); publicKey.write('ssh-ed25519', 4, 11, 'ascii'); writeUInt32BE(publicKey, pub.length, 15); pub.copy(publicKey, 19); return publicKey; } function genOpenSSLEdPriv(priv) { var 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) { var 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); Q.copy(asnWriter._buf, asnWriter._offset, 0, Q.length); asnWriter._offset += Q.length; // end hack asnWriter.endSequence(); asnWriter.endSequence(); return makePEM('PUBLIC', asnWriter.buffer); } function genOpenSSHECDSAPub(oid, Q) { var 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; } var publicKey = Buffer.allocUnsafe(4 + 19 // ecdsa-sha2- + 4 + 8 // + 4 + Q.length); writeUInt32BE(publicKey, 19, 0); publicKey.write('ecdsa-sha2-' + curveName, 4, 19, 'ascii'); writeUInt32BE(publicKey, 8, 23); publicKey.write(curveName, 27, 8, 'ascii'); writeUInt32BE(publicKey, Q.length, 35); Q.copy(publicKey, 39); return publicKey; } function genOpenSSLECDSAPriv(oid, pub, priv) { var 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); pub.copy(asnWriter._buf, asnWriter._offset, 0, pub.length); asnWriter._offset += pub.length; // end hack asnWriter.endSequence(); asnWriter.endSequence(); asnWriter.endSequence(); return makePEM('EC PRIVATE', asnWriter.buffer); } function genOpenSSLECDSAPubFromPriv(curveName, priv) { var tempECDH = crypto.createECDH(curveName); tempECDH.setPrivateKey(priv); return tempECDH.getPublicKey(); } var baseKeySign = (function() { if (typeof cryptoSign === 'function') { return function sign(data) { var pem = this[SYM_PRIV_PEM]; if (pem === null) return new Error('No private key available'); try { return cryptoSign(this[SYM_HASH_ALGO], data, pem); } catch (ex) { return ex; } }; } else { function trySign(signature, privKey) { try { return signature.sign(privKey); } catch (ex) { return ex; } } return function sign(data) { var pem = this[SYM_PRIV_PEM]; if (pem === null) return new Error('No private key available'); var signature = createSign(this[SYM_HASH_ALGO]); signature.update(data); return trySign(signature, pem); }; } })(); var baseKeyVerify = (function() { if (typeof cryptoVerify === 'function') { return function verify(data, signature) { var pem = this[SYM_PUB_PEM]; if (pem === null) return new Error('No public key available'); try { return cryptoVerify(this[SYM_HASH_ALGO], data, pem, signature); } catch (ex) { return ex; } }; } else { function tryVerify(verifier, pubKey, signature) { try { return verifier.verify(pubKey, signature); } catch (ex) { return ex; } } return function verify(data, signature) { var pem = this[SYM_PUB_PEM]; if (pem === null) return new Error('No public key available'); var verifier = createVerify(this[SYM_HASH_ALGO]); verifier.update(data); return tryVerify(verifier, pem, signature); }; } })(); var BaseKey = { sign: baseKeySign, verify: baseKeyVerify, getPrivatePEM: function getPrivatePEM() { return this[SYM_PRIV_PEM]; }, getPublicPEM: function getPublicPEM() { return this[SYM_PUB_PEM]; }, getPublicSSH: function getPublicSSH() { return this[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; (function() { var regexp = /^-----BEGIN OPENSSH PRIVATE KEY-----(?:\r\n|\n)([\s\S]+)(?:\r\n|\n)-----END OPENSSH PRIVATE KEY-----$/; OpenSSH_Private.parse = function(str, passphrase) { var m = regexp.exec(str); if (m === null) return null; var ret; var data = Buffer.from(m[1], 'base64'); if (data.length < 31) // magic (+ magic null term.) + minimum field lengths return new Error('Malformed OpenSSH private key'); var magic = data.toString('ascii', 0, 15); if (magic !== 'openssh-key-v1\0') return new Error('Unsupported OpenSSH key magic: ' + magic); // avoid cyclic require by requiring on first use if (!utils) utils = require('./utils'); var cipherName = utils.readString(data, 15, 'ascii'); if (cipherName === false) return new Error('Malformed OpenSSH private key'); if (cipherName !== 'none' && SUPPORTED_CIPHER.indexOf(cipherName) === -1) return new Error('Unsupported cipher for OpenSSH key: ' + cipherName); var kdfName = utils.readString(data, data._pos, 'ascii'); if (kdfName === false) 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'); } var encInfo; var cipherKey; var cipherIV; if (cipherName !== 'none') encInfo = CIPHER_INFO[cipherName]; var kdfOptions = utils.readString(data, data._pos); if (kdfOptions === false) 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 */ var salt = utils.readString(kdfOptions, 0); if (salt === false || kdfOptions._pos + 4 > kdfOptions.length) return new Error('Malformed OpenSSH private key'); var rounds = readUInt32BE(kdfOptions, kdfOptions._pos); var gen = Buffer.allocUnsafe(encInfo.keyLen + encInfo.ivLen); var 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 = gen.slice(0, encInfo.keyLen); cipherIV = gen.slice(encInfo.keyLen); break; } } else if (kdfName !== 'none') { return new Error('Malformed OpenSSH private key'); } var keyCount = utils.readInt(data, data._pos); if (keyCount === false) return new Error('Malformed OpenSSH private key'); data._pos += 4; if (keyCount > 0) { // TODO: place sensible limit on max `keyCount` // Read public keys first for (var i = 0; i < keyCount; ++i) { var pubData = utils.readString(data, data._pos); if (pubData === false) return new Error('Malformed OpenSSH private key'); var type = utils.readString(pubData, 0, 'ascii'); if (type === false) return new Error('Malformed OpenSSH private key'); } var privBlob = utils.readString(data, data._pos); if (privBlob === false) 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 { var options = { authTagLength: encInfo.authLen }; var decipher = createDecipheriv(SSH_TO_OPENSSL[cipherName], cipherKey, cipherIV, options); if (encInfo.authLen > 0) { if (data.length - data._pos < encInfo.authLen) return new Error('Malformed OpenSSH private key'); decipher.setAuthTag( data.slice(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 = []; } return ret; }; function parseOpenSSHPrivKeys(data, nkeys, decrypted) { var 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'); var check1 = readUInt32BE(data, 0); var 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; var i; var oid; for (i = 0; i < nkeys; ++i) { var algo = undefined; var privPEM = undefined; var pubPEM = undefined; var pubSSH = undefined; // 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 ... var type = utils.readString(data, data._pos, 'ascii'); if (type === false) 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 */ var n = utils.readString(data, data._pos); if (n === false) return new Error('Malformed OpenSSH private key'); var e = utils.readString(data, data._pos); if (e === false) return new Error('Malformed OpenSSH private key'); var d = utils.readString(data, data._pos); if (d === false) return new Error('Malformed OpenSSH private key'); var iqmp = utils.readString(data, data._pos); if (iqmp === false) return new Error('Malformed OpenSSH private key'); var p = utils.readString(data, data._pos); if (p === false) return new Error('Malformed OpenSSH private key'); var q = utils.readString(data, data._pos); if (q === false) 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 */ var p = utils.readString(data, data._pos); if (p === false) return new Error('Malformed OpenSSH private key'); var q = utils.readString(data, data._pos); if (q === false) return new Error('Malformed OpenSSH private key'); var g = utils.readString(data, data._pos); if (g === false) return new Error('Malformed OpenSSH private key'); var y = utils.readString(data, data._pos); if (y === false) return new Error('Malformed OpenSSH private key'); var x = utils.readString(data, data._pos); if (x === false) 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 (!EDDSA_SUPPORTED) return new Error('Unsupported OpenSSH private key type: ' + type); /* * string public key * string private key + public key */ var edpub = utils.readString(data, data._pos); if (edpub === false || edpub.length !== 32) return new Error('Malformed OpenSSH private key'); var edpriv = utils.readString(data, data._pos); if (edpriv === false || edpriv.length !== 64) return new Error('Malformed OpenSSH private key'); pubPEM = genOpenSSLEdPub(edpub); pubSSH = genOpenSSHEdPub(edpub); privPEM = genOpenSSLEdPriv(edpriv.slice(0, 32)); algo = null; break; case 'ecdsa-sha2-nistp256': algo = 'sha256'; oid = '1.2.840.10045.3.1.7'; case 'ecdsa-sha2-nistp384': if (algo === undefined) { algo = 'sha384'; oid = '1.3.132.0.34'; } 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'); var ecpub = utils.readString(data, data._pos); if (ecpub === false) return new Error('Malformed OpenSSH private key'); var ecpriv = utils.readString(data, data._pos); if (ecpriv === false) 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); } var privComment = utils.readString(data, data._pos, 'utf8'); if (privComment === false) return new Error('Malformed OpenSSH private key'); keys.push( new OpenSSH_Private(type, privComment, privPEM, pubPEM, pubSSH, algo, decrypted) ); } var 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; (function() { var 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 = function(str, passphrase) { var m = regexp.exec(str); if (m === null) return null; var privBlob = Buffer.from(m[3], 'base64'); var headers = m[2]; var decrypted = false; if (headers !== undefined) { // encrypted key headers = headers.split(/\r\n|\n/g); for (var i = 0; i < headers.length; ++i) { var header = headers[i]; var sepIdx = header.indexOf(':'); if (header.slice(0, sepIdx) === 'DEK-Info') { var val = header.slice(sepIdx + 2); sepIdx = val.indexOf(','); if (sepIdx === -1) continue; var cipherName = val.slice(0, sepIdx).toLowerCase(); if (supportedOpenSSLCiphers.indexOf(cipherName) === -1) { return new Error( 'Cipher (' + cipherName + ') not supported for encrypted OpenSSH private key' ); } var encInfo = CIPHER_INFO_OPENSSL[cipherName]; if (!encInfo) { return new Error( 'Cipher (' + cipherName + ') not supported for encrypted OpenSSH private key' ); } var 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' ); } var cipherKey = createHash('md5') .update(passphrase) .update(cipherIV.slice(0, 8)) .digest(); while (cipherKey.length < encInfo.keyLen) { cipherKey = combineBuffers( cipherKey, (createHash('md5') .update(cipherKey) .update(passphrase) .update(cipherIV) .digest()).slice(0, 8) ); } if (cipherKey.length > encInfo.keyLen) cipherKey = cipherKey.slice(0, encInfo.keyLen); try { var decipher = createDecipheriv(cipherName, cipherKey, cipherIV); decipher.setAutoPadding(false); privBlob = combineBuffers(decipher.update(privBlob), decipher.final()); decrypted = true; } catch (ex) { return ex; } } } } var type; var privPEM; var pubPEM; var pubSSH; var algo; var reader; var 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 var n = reader.readString(Ber.Integer, true); if (n === null) return new Error(errMsg); var e = reader.readString(Ber.Integer, true); if (e === null) return new Error(errMsg); pubPEM = genOpenSSLRSAPub(n, e); pubSSH = genOpenSSHRSAPub(n, e); } catch (ex) { 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 var p = reader.readString(Ber.Integer, true); if (p === null) return new Error(errMsg); var q = reader.readString(Ber.Integer, true); if (q === null) return new Error(errMsg); var g = reader.readString(Ber.Integer, true); if (g === null) return new Error(errMsg); var 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 (ex) { return new Error(errMsg); } algo = 'sha1'; break; case 'EC': var ecSSLName; var ecPriv; 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 var offset = reader.readLength(); // Skip context length if (offset !== null) { reader._offset = offset; var oid = reader.readOID(); if (oid === null) return new Error(errMsg); switch (oid) { 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: ' + oid); } } else { return new Error(errMsg); } } catch (ex) { return new Error(errMsg); } privPEM = makePEM('EC PRIVATE', privBlob); var pubBlob = genOpenSSLECDSAPubFromPriv(ecSSLName, ecPriv); pubPEM = genOpenSSLECDSAPub(oid, pubBlob); pubSSH = genOpenSSHECDSAPub(oid, 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; (function() { var EMPTY_PASSPHRASE = Buffer.alloc(0); var PPK_IV = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); var PPK_PP1 = Buffer.from([0, 0, 0, 0]); var PPK_PP2 = Buffer.from([0, 0, 0, 1]); var 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 = function(str, passphrase) { var 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) var cipherName = m[2]; var encrypted = (cipherName !== 'none'); if (encrypted && !passphrase) { return new Error( 'Encrypted PPK private key detected, but no passphrase given' ); } var privBlob = Buffer.from(m[5], 'base64'); if (encrypted) { var encInfo = CIPHER_INFO[cipherName]; var cipherKey = combineBuffers( createHash('sha1').update(PPK_PP1).update(passphrase).digest(), createHash('sha1').update(PPK_PP2).update(passphrase).digest() ); if (cipherKey.length > encInfo.keyLen) cipherKey = cipherKey.slice(0, encInfo.keyLen); try { var decipher = createDecipheriv(SSH_TO_OPENSSL[cipherName], cipherKey, PPK_IV); decipher.setAutoPadding(false); privBlob = combineBuffers(decipher.update(privBlob), decipher.final()); decrypted = true; } catch (ex) { return ex; } } var type = m[1]; var comment = m[3]; var pubBlob = Buffer.from(m[4], 'base64'); var mac = m[6]; var typeLen = type.length; var cipherNameLen = cipherName.length; var commentLen = Buffer.byteLength(comment); var pubLen = pubBlob.length; var privLen = privBlob.length; var macData = Buffer.allocUnsafe(4 + typeLen + 4 + cipherNameLen + 4 + commentLen + 4 + pubLen + 4 + privLen); var p = 0; writeUInt32BE(macData, typeLen, p); macData.write(type, p += 4, typeLen, 'ascii'); writeUInt32BE(macData, cipherNameLen, p += typeLen); macData.write(cipherName, p += 4, cipherNameLen, 'ascii'); writeUInt32BE(macData, commentLen, p += cipherNameLen); macData.write(comment, p += 4, commentLen, 'utf8'); writeUInt32BE(macData, pubLen, p += commentLen); pubBlob.copy(macData, p += 4); writeUInt32BE(macData, privLen, p += pubLen); privBlob.copy(macData, p + 4); if (!passphrase) passphrase = EMPTY_PASSPHRASE; var 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?' ); } else { return new Error('PPK private key integrity check failed'); } } // avoid cyclic require by requiring on first use if (!utils) utils = require('./utils'); var pubPEM; var pubSSH; var privPEM; pubBlob._pos = 0; skipFields(pubBlob, 1); // skip (duplicate) key type switch (type) { case 'ssh-rsa': var e = utils.readString(pubBlob, pubBlob._pos); if (e === false) return new Error('Malformed PPK public key'); var n = utils.readString(pubBlob, pubBlob._pos); if (n === false) return new Error('Malformed PPK public key'); var d = utils.readString(privBlob, 0); if (d === false) return new Error('Malformed PPK private key'); var p = utils.readString(privBlob, privBlob._pos); if (p === false) return new Error('Malformed PPK private key'); var q = utils.readString(privBlob, privBlob._pos); if (q === false) return new Error('Malformed PPK private key'); var iqmp = utils.readString(privBlob, privBlob._pos); if (iqmp === false) 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': var p = utils.readString(pubBlob, pubBlob._pos); if (p === false) return new Error('Malformed PPK public key'); var q = utils.readString(pubBlob, pubBlob._pos); if (q === false) return new Error('Malformed PPK public key'); var g = utils.readString(pubBlob, pubBlob._pos); if (g === false) return new Error('Malformed PPK public key'); var y = utils.readString(pubBlob, pubBlob._pos); if (y === false) return new Error('Malformed PPK public key'); var x = utils.readString(privBlob, 0); if (x === false) 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 parseDER(data, baseType, comment, fullType) { // avoid cyclic require by requiring on first use if (!utils) utils = require('./utils'); var algo; var pubPEM = null; var pubSSH = null; switch (baseType) { case 'ssh-rsa': var e = utils.readString(data, data._pos); if (e === false) return new Error('Malformed OpenSSH public key'); var n = utils.readString(data, data._pos); if (n === false) return new Error('Malformed OpenSSH public key'); pubPEM = genOpenSSLRSAPub(n, e); pubSSH = genOpenSSHRSAPub(n, e); algo = 'sha1'; break; case 'ssh-dss': var p = utils.readString(data, data._pos); if (p === false) return new Error('Malformed OpenSSH public key'); var q = utils.readString(data, data._pos); if (q === false) return new Error('Malformed OpenSSH public key'); var g = utils.readString(data, data._pos); if (g === false) return new Error('Malformed OpenSSH public key'); var y = utils.readString(data, data._pos); if (y === false) 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': var edpub = utils.readString(data, data._pos); if (edpub === false || 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'; case 'ecdsa-sha2-nistp384': if (algo === undefined) { algo = 'sha384'; oid = '1.3.132.0.34'; } 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'); var ecpub = utils.readString(data, data._pos); if (ecpub === false) 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 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; (function() { var regexp; if (EDDSA_SUPPORTED) 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 = function(str) { var 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 // avoid cyclic require by requiring on first use if (!utils) utils = require('./utils'); var fullType = m[1]; var baseType = m[2]; var data = Buffer.from(m[3], 'base64'); var comment = (m[4] || ''); var type = utils.readString(data, data._pos, 'ascii'); if (type === false || 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; (function() { var regexp = /^---- BEGIN SSH2 PUBLIC KEY ----(?:\r\n|\n)((?:(?:[\x21-\x7E]+?):(?:(?:.*?\\\r?\n)*.*)(?:\r\n|\n))*)((?:[A-Z0-9a-z\/+=]+(?:\r\n|\n))+)---- END SSH2 PUBLIC KEY ----$/; var RE_HEADER = /^([\x21-\x7E]+?):((?:.*?\\\r?\n)*.*)$/gm; var RE_HEADER_ENDS = /\\\r?\n/g; RFC4716_Public.parse = function(str) { var m = regexp.exec(str); if (m === null) return null; // m[1] = header(s) // m[2] = base64-encoded public key var headers = m[1]; var data = Buffer.from(m[2], 'base64'); var comment = ''; if (headers !== undefined) { while (m = RE_HEADER.exec(headers)) { if (m[1].toLowerCase() === 'comment') { comment = trimStart(m[2].replace(RE_HEADER_ENDS, '')); if (comment.length > 1 && comment.charCodeAt(0) === 34/*'"'*/ && comment.charCodeAt(comment.length - 1) === 34/*'"'*/) { comment = comment.slice(1, -1); } } } } // avoid cyclic require by requiring on first use if (!utils) utils = require('./utils'); var type = utils.readString(data, 0, 'ascii'); if (type === false) return new Error('Malformed RFC4716 public key'); var pubPEM = null; var pubSSH = null; switch (type) { case 'ssh-rsa': var e = utils.readString(data, data._pos); if (e === false) return new Error('Malformed RFC4716 public key'); var n = utils.readString(data, data._pos); if (n === false) return new Error('Malformed RFC4716 public key'); pubPEM = genOpenSSLRSAPub(n, e); pubSSH = genOpenSSHRSAPub(n, e); break; case 'ssh-dss': var p = utils.readString(data, data._pos); if (p === false) return new Error('Malformed RFC4716 public key'); var q = utils.readString(data, data._pos); if (q === false) return new Error('Malformed RFC4716 public key'); var g = utils.readString(data, data._pos); if (g === false) return new Error('Malformed RFC4716 public key'); var y = utils.readString(data, data._pos); if (y === false) 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'); }; })(); module.exports = { parseDERKey: function parseDERKey(data, type) { return parseDER(data, type, '', type); }, parseKey: function parseKey(data, passphrase) { if (Buffer.isBuffer(data)) data = data.toString('utf8').trim(); else if (typeof data !== 'string') return new Error('Key data must be a Buffer or string'); else data = data.trim(); // intentional != 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'); } var ret; // 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; return new Error('Unsupported key format'); } }