// TODO: // * handle multi-line header values (OpenSSH)? // * more thorough validation? var utils; var Ber = require('asn1').Ber; var semver = require('semver'); var RE_PPK = /^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]+)/; var RE_HEADER_OPENSSH_PRIV = /^-----BEGIN (RSA|DSA|EC) PRIVATE KEY-----$/i; var RE_FOOTER_OPENSSH_PRIV = /^-----END (?:RSA|DSA|EC) PRIVATE KEY-----$/i; var RE_HEADER_OPENSSH_PUB = /^((?:(?:ssh-(rsa|dss))|ecdsa-sha2-nistp(256|384|521))(?:-cert-v0[01]@openssh.com)?) ([A-Z0-9a-z\/+=]+)(?:$|\s+([\S].*)?)$/i; var RE_HEADER_RFC4716_PUB = /^---- BEGIN SSH2 PUBLIC KEY ----$/i; var RE_FOOTER_RFC4716_PUB = /^---- END SSH2 PUBLIC KEY ----$/i; var RE_HEADER_OPENSSH = /^([^:]+):\s*([\S].*)?$/i; var RE_HEADER_RFC4716 = /^([^:]+): (.*)?$/i; module.exports = function(data) { if (Buffer.isBuffer(data)) data = data.toString('utf8'); else if (typeof data !== 'string') return new Error('Key data must be a Buffer or string'); var ret = { fulltype: undefined, type: undefined, curve: undefined, extra: undefined, comment: undefined, encryption: undefined, private: undefined, privateOrig: undefined, public: undefined, publicOrig: undefined }; var m; var i; var len; data = data.trim().split(/\r\n|\n/); while (!data[0].length) data.shift(); while (!data.slice(-1)[0].length) data.pop(); var orig = data.join('\n'); if ((m = RE_HEADER_OPENSSH_PRIV.exec(data[0])) && RE_FOOTER_OPENSSH_PRIV.test(data.slice(-1))) { // OpenSSH private key var keyType = m[1].toLowerCase(); if (keyType === 'dsa') keyType = 'dss'; if (keyType === 'ec' && semver.lt(process.version, '5.2.0')) { return new Error( 'EC private keys are not supported in this version of node' ); } if (!RE_HEADER_OPENSSH.test(data[1])) { // unencrypted, no headers var privData = new Buffer(data.slice(1, -1).join(''), 'base64'); if (keyType !== 'ec') { ret.fulltype = 'ssh-' + keyType; } else { // ECDSA var asnReader = new Ber.Reader(privData); asnReader.readSequence(); asnReader.readInt(); asnReader.readString(Ber.OctetString, true); asnReader.readByte(); // Skip "complex" context type byte var offset = asnReader.readLength(); // Skip context length if (offset !== null) { asnReader._offset = offset; switch (asnReader.readOID()) { case '1.2.840.10045.3.1.7': // prime256v1/secp256r1 ret.fulltype = 'ecdsa-sha2-nistp256'; break; case '1.3.132.0.34': // secp384r1 ret.fulltype = 'ecdsa-sha2-nistp384'; break; case '1.3.132.0.35': // secp521r1 ret.fulltype = 'ecdsa-sha2-nistp521'; break; } } if (ret.fulltype === undefined) return new Error('Unsupported EC private key type'); } ret.private = privData; } else { // possibly encrypted, headers for (i = 1, len = data.length; i < len; ++i) { m = RE_HEADER_OPENSSH.exec(data[i]); if (m) { m[1] = m[1].toLowerCase(); if (m[1] === 'dek-info') { m[2] = m[2].split(','); ret.encryption = m[2][0].toLowerCase(); if (m[2].length > 1) ret.extra = m[2].slice(1); } } else if (data[i].length) break; } ret.private = new Buffer(data.slice(i, -1).join(''), 'base64'); } ret.type = keyType; ret.privateOrig = new Buffer(orig); } else if (m = RE_HEADER_OPENSSH_PUB.exec(data[0])) { // OpenSSH public key ret.fulltype = m[1]; ret.type = (m[2] || 'ec').toLowerCase(); ret.public = new Buffer(m[4], 'base64'); ret.publicOrig = new Buffer(orig); ret.comment = m[5]; if (m[3]) // ECDSA only ret.curve = 'nistp' + m[3]; } else if (RE_HEADER_RFC4716_PUB.test(data[0]) && RE_FOOTER_RFC4716_PUB.test(data.slice(-1))) { if (data[1].indexOf(': ') === -1) { // no headers ret.public = new Buffer(data.slice(1, -1).join(''), 'base64'); } else { // headers for (i = 1, len = data.length; i < len; ++i) { if (data[i].indexOf(': ') === -1) { if (data[i].length) break; // start of key data else continue; // empty line } while (data[i].substr(-1) === '\\') { if (i + 1 < len) { data[i] = data[i].slice(0, -1) + data[i + 1]; data.splice(i + 1, 1); --len; } else return new Error('RFC4716 public key missing header continuation line'); } m = RE_HEADER_RFC4716.exec(data[i]); if (m) { m[1] = m[1].toLowerCase(); if (m[1] === 'comment') { ret.comment = m[2] || ''; if (ret.comment[0] === '"' && ret.comment.substr(-1) === '"') ret.comment = ret.comment.slice(1, -1); } } else return new Error('RFC4716 public key invalid header line'); } ret.public = new Buffer(data.slice(i, -1).join(''), 'base64'); } len = ret.public.readUInt32BE(0, true); var fulltype = ret.public.toString('ascii', 4, 4 + len); ret.fulltype = fulltype; if (fulltype === 'ssh-dss') ret.type = 'dss'; else if (fulltype === 'ssh-rsa') ret.type = 'rsa'; else return new Error('Unsupported RFC4716 public key type: ' + fulltype); ret.public = ret.public.slice(11); ret.publicOrig = new Buffer(orig); } else if (m = RE_PPK.exec(orig)) { // m[1] = short 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) // avoid cyclic require by requiring on first use if (!utils) utils = require('./utils'); ret.ppk = true; ret.type = m[1]; ret.fulltype = 'ssh-' + m[1]; if (m[2] !== 'none') ret.encryption = m[2]; ret.comment = m[3]; ret.public = new Buffer(m[4].replace(/\r?\n/g, ''), 'base64'); var privateKey = new Buffer(m[5].replace(/\r?\n/g, ''), 'base64'); ret.privateMAC = m[6].replace(/\r?\n/g, ''); // automatically verify private key MAC if we don't need to wait for // decryption if (!ret.encryption) { var valid = utils.verifyPPKMAC(ret, undefined, privateKey); if (!valid) throw new Error('PPK MAC mismatch'); } // generate a PEM encoded version of the public key var pubkey = utils.genPublicKey(ret); ret.public = pubkey.public; ret.publicOrig = pubkey.publicOrig; ret.private = privateKey; // automatically convert private key data to OpenSSL format (including PEM) // if we don't need to wait for decryption if (!ret.encryption) utils.convertPPKPrivate(ret); } else return new Error('Unsupported key format'); return ret; };