2023-10-03 11:14:36 +08:00
|
|
|
// 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;
|
|
|
|
};
|