2023-10-03 11:14:36 +08:00
|
|
|
var crypto = require('crypto');
|
|
|
|
|
|
|
|
var Ber = require('asn1').Ber;
|
|
|
|
var BigInteger = require('./jsbn'); // only for converting PPK -> OpenSSL format
|
|
|
|
|
|
|
|
var SSH_TO_OPENSSL = require('./constants').SSH_TO_OPENSSL;
|
|
|
|
|
|
|
|
var RE_STREAM = /^arcfour/i;
|
|
|
|
var RE_KEY_LEN = /(.{64})/g;
|
|
|
|
// XXX the value of 2400 from dropbear is only for certain strings, not all
|
|
|
|
// strings. for example the list strings used during handshakes
|
|
|
|
var MAX_STRING_LEN = Infinity;//2400; // taken from dropbear
|
|
|
|
var PPK_IV = new Buffer([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
iv_inc: iv_inc,
|
|
|
|
isStreamCipher: isStreamCipher,
|
|
|
|
readInt: readInt,
|
|
|
|
readString: readString,
|
|
|
|
parseKey: require('./keyParser'),
|
|
|
|
genPublicKey: genPublicKey,
|
|
|
|
convertPPKPrivate: convertPPKPrivate,
|
|
|
|
verifyPPKMAC: verifyPPKMAC,
|
|
|
|
decryptKey: decryptKey,
|
|
|
|
DSASigBERToBare: DSASigBERToBare,
|
|
|
|
DSASigBareToBER: DSASigBareToBER,
|
|
|
|
ECDSASigASN1ToSSH: ECDSASigASN1ToSSH,
|
|
|
|
ECDSASigSSHToASN1: ECDSASigSSHToASN1,
|
|
|
|
RSAKeySSHToASN1: RSAKeySSHToASN1,
|
|
|
|
DSAKeySSHToASN1: DSAKeySSHToASN1,
|
|
|
|
ECDSAKeySSHToASN1: ECDSAKeySSHToASN1
|
|
|
|
};
|
|
|
|
|
|
|
|
function iv_inc(iv) {
|
|
|
|
var n = 12;
|
|
|
|
var c = 0;
|
|
|
|
do {
|
|
|
|
--n;
|
|
|
|
c = iv[n];
|
|
|
|
if (c === 255)
|
|
|
|
iv[n] = 0;
|
|
|
|
else {
|
|
|
|
iv[n] = ++c;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} while (n > 4);
|
|
|
|
}
|
|
|
|
|
|
|
|
function isStreamCipher(name) {
|
|
|
|
return RE_STREAM.test(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
function readInt(buffer, start, stream, cb) {
|
|
|
|
var bufferLen = buffer.length;
|
|
|
|
if (start < 0 || start >= bufferLen || (bufferLen - start) < 4) {
|
|
|
|
stream && stream._cleanup(cb);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return buffer.readUInt32BE(start, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
function DSASigBERToBare(signature) {
|
|
|
|
if (signature.length <= 40)
|
|
|
|
return signature;
|
|
|
|
// This is a quick and dirty way to get from BER encoded r and s that
|
|
|
|
// OpenSSL gives us, to just the bare values back to back (40 bytes
|
|
|
|
// total) like OpenSSH (and possibly others) are expecting
|
|
|
|
var asnReader = new Ber.Reader(signature);
|
|
|
|
asnReader.readSequence();
|
|
|
|
var r = asnReader.readString(Ber.Integer, true);
|
|
|
|
var s = asnReader.readString(Ber.Integer, true);
|
|
|
|
var rOffset = 0;
|
|
|
|
var sOffset = 0;
|
|
|
|
if (r.length < 20) {
|
|
|
|
var rNew = new Buffer(20);
|
|
|
|
r.copy(rNew, 1);
|
|
|
|
r = rNew;
|
|
|
|
r[0] = 0;
|
|
|
|
}
|
|
|
|
if (s.length < 20) {
|
|
|
|
var sNew = new Buffer(20);
|
|
|
|
s.copy(sNew, 1);
|
|
|
|
s = sNew;
|
|
|
|
s[0] = 0;
|
|
|
|
}
|
|
|
|
if (r.length > 20 && r[0] === 0x00)
|
|
|
|
rOffset = 1;
|
|
|
|
if (s.length > 20 && s[0] === 0x00)
|
|
|
|
sOffset = 1;
|
|
|
|
var newSig = new Buffer((r.length - rOffset) + (s.length - sOffset));
|
|
|
|
r.copy(newSig, 0, rOffset);
|
|
|
|
s.copy(newSig, r.length - rOffset, sOffset);
|
|
|
|
return newSig;
|
|
|
|
}
|
|
|
|
|
|
|
|
function DSASigBareToBER(signature) {
|
|
|
|
if (signature.length > 40)
|
|
|
|
return signature;
|
|
|
|
// Change bare signature r and s values to ASN.1 BER values for OpenSSL
|
|
|
|
var asnWriter = new Ber.Writer();
|
|
|
|
asnWriter.startSequence();
|
|
|
|
var r = signature.slice(0, 20);
|
|
|
|
var s = signature.slice(20);
|
|
|
|
if (r[0] & 0x80) {
|
|
|
|
var rNew = new Buffer(21);
|
|
|
|
rNew[0] = 0x00;
|
|
|
|
r.copy(rNew, 1);
|
|
|
|
r = rNew;
|
|
|
|
} else if (r[0] === 0x00 && !(r[1] & 0x80)) {
|
|
|
|
r = r.slice(1);
|
|
|
|
}
|
|
|
|
if (s[0] & 0x80) {
|
|
|
|
var sNew = new Buffer(21);
|
|
|
|
sNew[0] = 0x00;
|
|
|
|
s.copy(sNew, 1);
|
|
|
|
s = sNew;
|
|
|
|
} else if (s[0] === 0x00 && !(s[1] & 0x80)) {
|
|
|
|
s = s.slice(1);
|
|
|
|
}
|
|
|
|
asnWriter.writeBuffer(r, Ber.Integer);
|
|
|
|
asnWriter.writeBuffer(s, Ber.Integer);
|
|
|
|
asnWriter.endSequence();
|
|
|
|
return asnWriter.buffer;
|
|
|
|
}
|
|
|
|
|
|
|
|
function ECDSASigASN1ToSSH(signature) {
|
|
|
|
if (signature[0] === 0x00)
|
|
|
|
return signature;
|
|
|
|
// Convert SSH signature parameters to ASN.1 BER values for OpenSSL
|
|
|
|
var asnReader = new Ber.Reader(signature);
|
|
|
|
asnReader.readSequence();
|
|
|
|
var r = asnReader.readString(Ber.Integer, true);
|
|
|
|
var s = asnReader.readString(Ber.Integer, true);
|
|
|
|
if (r === null || s === null)
|
|
|
|
throw new Error('Invalid signature');
|
|
|
|
var newSig = new Buffer(4 + r.length + 4 + s.length);
|
|
|
|
newSig.writeUInt32BE(r.length, 0, true);
|
|
|
|
r.copy(newSig, 4);
|
|
|
|
newSig.writeUInt32BE(s.length, 4 + r.length, true);
|
|
|
|
s.copy(newSig, 4 + 4 + r.length);
|
|
|
|
return newSig;
|
|
|
|
}
|
|
|
|
|
|
|
|
function ECDSASigSSHToASN1(signature, self, callback) {
|
|
|
|
// Convert SSH signature parameters to ASN.1 BER values for OpenSSL
|
|
|
|
var r = readString(signature, 0, self, callback);
|
|
|
|
if (r === false)
|
|
|
|
return false;
|
|
|
|
var s = readString(signature, signature._pos, self, callback);
|
|
|
|
if (s === false)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
var asnWriter = new Ber.Writer();
|
|
|
|
asnWriter.startSequence();
|
|
|
|
asnWriter.writeBuffer(r, Ber.Integer);
|
|
|
|
asnWriter.writeBuffer(s, Ber.Integer);
|
|
|
|
asnWriter.endSequence();
|
|
|
|
return asnWriter.buffer;
|
|
|
|
}
|
|
|
|
|
|
|
|
function RSAKeySSHToASN1(key, self, callback) {
|
|
|
|
// Convert SSH key parameters to ASN.1 BER values for OpenSSL
|
|
|
|
var e = readString(key, key._pos, self, callback);
|
|
|
|
if (e === false)
|
|
|
|
return false;
|
|
|
|
var n = readString(key, key._pos, self, callback);
|
|
|
|
if (n === false)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
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 asnWriter.buffer;
|
|
|
|
}
|
|
|
|
|
|
|
|
function DSAKeySSHToASN1(key, self, callback) {
|
|
|
|
// Convert SSH key parameters to ASN.1 BER values for OpenSSL
|
|
|
|
var p = readString(key, key._pos, self, callback);
|
|
|
|
if (p === false)
|
|
|
|
return false;
|
|
|
|
var q = readString(key, key._pos, self, callback);
|
|
|
|
if (q === false)
|
|
|
|
return false;
|
|
|
|
var g = readString(key, key._pos, self, callback);
|
|
|
|
if (g === false)
|
|
|
|
return false;
|
|
|
|
var y = readString(key, key._pos, self, callback);
|
|
|
|
if (y === false)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
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 asnWriter.buffer;
|
|
|
|
}
|
|
|
|
|
|
|
|
function ECDSAKeySSHToASN1(key, self, callback) {
|
|
|
|
// Convert SSH key parameters to ASN.1 BER values for OpenSSL
|
|
|
|
var curve = readString(key, key._pos, self, callback);
|
|
|
|
if (curve === false)
|
|
|
|
return false;
|
|
|
|
var Q = readString(key, key._pos, self, callback);
|
|
|
|
if (Q === false)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
var ecCurveOID;
|
|
|
|
switch (curve.toString('ascii')) {
|
|
|
|
case 'nistp256':
|
|
|
|
// prime256v1/secp256r1
|
|
|
|
ecCurveOID = '1.2.840.10045.3.1.7';
|
|
|
|
break;
|
|
|
|
case 'nistp384':
|
|
|
|
// secp384r1
|
|
|
|
ecCurveOID = '1.3.132.0.34';
|
|
|
|
break;
|
|
|
|
case 'nistp521':
|
|
|
|
// secp521r1
|
|
|
|
ecCurveOID = '1.3.132.0.35';
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
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(ecCurveOID);
|
|
|
|
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 asnWriter.buffer;
|
|
|
|
}
|
|
|
|
|
|
|
|
function decryptKey(keyInfo, passphrase) {
|
|
|
|
if (keyInfo._decrypted || !keyInfo.encryption)
|
|
|
|
return;
|
|
|
|
|
|
|
|
var keylen = 0;
|
|
|
|
var key;
|
|
|
|
var iv;
|
|
|
|
var dc;
|
|
|
|
|
|
|
|
keyInfo.encryption = (SSH_TO_OPENSSL[keyInfo.encryption]
|
|
|
|
|| keyInfo.encryption);
|
|
|
|
switch (keyInfo.encryption) {
|
|
|
|
case 'aes-256-cbc':
|
|
|
|
case 'aes-256-ctr':
|
|
|
|
keylen = 32;
|
|
|
|
break;
|
|
|
|
case 'des-ede3-cbc':
|
|
|
|
case 'des-ede3':
|
|
|
|
case 'aes-192-cbc':
|
|
|
|
case 'aes-192-ctr':
|
|
|
|
keylen = 24;
|
|
|
|
break;
|
|
|
|
case 'aes-128-cbc':
|
|
|
|
case 'aes-128-ctr':
|
|
|
|
case 'cast-cbc':
|
|
|
|
case 'bf-cbc':
|
|
|
|
keylen = 16;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Error('Unsupported cipher for encrypted key: '
|
|
|
|
+ keyInfo.encryption);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (keyInfo.ppk) {
|
|
|
|
iv = PPK_IV;
|
|
|
|
|
|
|
|
key = Buffer.concat([
|
|
|
|
crypto.createHash('sha1')
|
|
|
|
.update('\x00\x00\x00\x00' + passphrase, 'utf8')
|
|
|
|
.digest(),
|
|
|
|
crypto.createHash('sha1')
|
|
|
|
.update('\x00\x00\x00\x01' + passphrase, 'utf8')
|
|
|
|
.digest()
|
|
|
|
]);
|
|
|
|
key = key.slice(0, keylen);
|
|
|
|
} else {
|
|
|
|
iv = new Buffer(keyInfo.extra[0], 'hex');
|
|
|
|
|
|
|
|
key = crypto.createHash('md5')
|
|
|
|
.update(passphrase, 'utf8')
|
|
|
|
.update(iv.slice(0, 8))
|
|
|
|
.digest();
|
|
|
|
|
|
|
|
while (keylen > key.length) {
|
|
|
|
key = Buffer.concat([
|
|
|
|
key,
|
|
|
|
(crypto.createHash('md5')
|
|
|
|
.update(key)
|
|
|
|
.update(passphrase, 'utf8')
|
|
|
|
.update(iv)
|
|
|
|
.digest()).slice(0, 8)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
if (key.length > keylen)
|
|
|
|
key = key.slice(0, keylen);
|
|
|
|
}
|
|
|
|
|
|
|
|
dc = crypto.createDecipheriv(keyInfo.encryption, key, iv);
|
|
|
|
dc.setAutoPadding(false);
|
|
|
|
keyInfo.private = Buffer.concat([ dc.update(keyInfo.private), dc.final() ]);
|
|
|
|
|
|
|
|
keyInfo._decrypted = true;
|
|
|
|
|
|
|
|
if (keyInfo.privateOrig) {
|
|
|
|
// Update our original base64-encoded version of the private key
|
|
|
|
var orig = keyInfo.privateOrig.toString('utf8');
|
|
|
|
var newOrig = /^(.+(?:\r\n|\n))/.exec(orig)[1];
|
|
|
|
var b64key = keyInfo.private.toString('base64');
|
|
|
|
|
|
|
|
newOrig += b64key.match(/.{1,70}/g).join('\n');
|
|
|
|
newOrig += /((?:\r\n|\n).+)$/.exec(orig)[1];
|
|
|
|
|
|
|
|
keyInfo.privateOrig = newOrig;
|
|
|
|
} else if (keyInfo.ppk) {
|
|
|
|
var valid = verifyPPKMAC(keyInfo, passphrase, keyInfo.private);
|
|
|
|
if (!valid)
|
|
|
|
throw new Error('PPK MAC mismatch');
|
|
|
|
// Automatically convert private key data to OpenSSL format
|
|
|
|
// (including PEM)
|
|
|
|
convertPPKPrivate(keyInfo);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fill in full key type
|
|
|
|
// TODO: make DRY, we do this also in keyParser
|
|
|
|
if (keyInfo.type !== 'ec') {
|
|
|
|
keyInfo.fulltype = 'ssh-' + keyInfo.type;
|
|
|
|
} else {
|
|
|
|
// ECDSA
|
|
|
|
var asnReader = new Ber.Reader(keyInfo.private);
|
|
|
|
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
|
|
|
|
keyInfo.fulltype = 'ecdsa-sha2-nistp256';
|
|
|
|
break;
|
|
|
|
case '1.3.132.0.34':
|
|
|
|
// secp384r1
|
|
|
|
keyInfo.fulltype = 'ecdsa-sha2-nistp384';
|
|
|
|
break;
|
|
|
|
case '1.3.132.0.35':
|
|
|
|
// secp521r1
|
|
|
|
keyInfo.fulltype = 'ecdsa-sha2-nistp521';
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (keyInfo.fulltype === undefined)
|
|
|
|
return new Error('Unsupported EC private key type');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function genPublicKey(keyInfo) {
|
|
|
|
var publicKey;
|
|
|
|
var i;
|
|
|
|
|
|
|
|
// RSA
|
|
|
|
var n;
|
|
|
|
var e;
|
|
|
|
|
|
|
|
// DSA
|
|
|
|
var p;
|
|
|
|
var q;
|
|
|
|
var g;
|
|
|
|
var y;
|
|
|
|
|
|
|
|
// ECDSA
|
|
|
|
var d;
|
|
|
|
var Q;
|
|
|
|
var ecCurveOID;
|
|
|
|
var ecCurveName;
|
|
|
|
|
|
|
|
if (keyInfo.private) {
|
|
|
|
// parsing private key in ASN.1 format in order to generate a public key
|
|
|
|
var privKey = keyInfo.private;
|
|
|
|
var asnReader = new Ber.Reader(privKey);
|
|
|
|
var errMsg;
|
|
|
|
|
|
|
|
if (asnReader.readSequence() === null) {
|
|
|
|
errMsg = 'Malformed private key (expected sequence)';
|
|
|
|
if (keyInfo._decrypted)
|
|
|
|
errMsg += '. Bad passphrase?';
|
|
|
|
throw new Error(errMsg);
|
|
|
|
}
|
|
|
|
|
|
|
|
// version (ignored)
|
|
|
|
if (asnReader.readInt() === null) {
|
|
|
|
errMsg = 'Malformed private key (expected version)';
|
|
|
|
if (keyInfo._decrypted)
|
|
|
|
errMsg += '. Bad passphrase?';
|
|
|
|
throw new Error(errMsg);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (keyInfo.type === 'rsa') {
|
|
|
|
// modulus (n) -- integer
|
|
|
|
n = asnReader.readString(Ber.Integer, true);
|
|
|
|
if (n === null) {
|
|
|
|
errMsg = 'Malformed private key (expected RSA n value)';
|
|
|
|
if (keyInfo._decrypted)
|
|
|
|
errMsg += '. Bad passphrase?';
|
|
|
|
throw new Error(errMsg);
|
|
|
|
}
|
|
|
|
|
|
|
|
// public exponent (e) -- integer
|
|
|
|
e = asnReader.readString(Ber.Integer, true);
|
|
|
|
if (e === null) {
|
|
|
|
errMsg = 'Malformed private key (expected RSA e value)';
|
|
|
|
if (keyInfo._decrypted)
|
|
|
|
errMsg += '. Bad passphrase?';
|
|
|
|
throw new Error(errMsg);
|
|
|
|
}
|
|
|
|
|
|
|
|
publicKey = new Buffer(4 + 7 // ssh-rsa
|
|
|
|
+ 4 + n.length
|
|
|
|
+ 4 + e.length);
|
|
|
|
|
|
|
|
publicKey.writeUInt32BE(7, 0, true);
|
|
|
|
publicKey.write('ssh-rsa', 4, 7, 'ascii');
|
|
|
|
|
|
|
|
i = 4 + 7;
|
|
|
|
publicKey.writeUInt32BE(e.length, i, true);
|
|
|
|
e.copy(publicKey, i += 4);
|
|
|
|
|
|
|
|
publicKey.writeUInt32BE(n.length, i += e.length, true);
|
|
|
|
n.copy(publicKey, i += 4);
|
|
|
|
} else if (keyInfo.type === 'dss') { // DSA
|
|
|
|
// prime (p) -- integer
|
|
|
|
p = asnReader.readString(Ber.Integer, true);
|
|
|
|
if (p === null) {
|
|
|
|
errMsg = 'Malformed private key (expected DSA p value)';
|
|
|
|
if (keyInfo._decrypted)
|
|
|
|
errMsg += '. Bad passphrase?';
|
|
|
|
throw new Error(errMsg);
|
|
|
|
}
|
|
|
|
|
|
|
|
// group order (q) -- integer
|
|
|
|
q = asnReader.readString(Ber.Integer, true);
|
|
|
|
if (q === null) {
|
|
|
|
errMsg = 'Malformed private key (expected DSA q value)';
|
|
|
|
if (keyInfo._decrypted)
|
|
|
|
errMsg += '. Bad passphrase?';
|
|
|
|
throw new Error(errMsg);
|
|
|
|
}
|
|
|
|
|
|
|
|
// group generator (g) -- integer
|
|
|
|
g = asnReader.readString(Ber.Integer, true);
|
|
|
|
if (g === null) {
|
|
|
|
errMsg = 'Malformed private key (expected DSA g value)';
|
|
|
|
if (keyInfo._decrypted)
|
|
|
|
errMsg += '. Bad passphrase?';
|
|
|
|
throw new Error(errMsg);
|
|
|
|
}
|
|
|
|
|
|
|
|
// public key value (y) -- integer
|
|
|
|
y = asnReader.readString(Ber.Integer, true);
|
|
|
|
if (y === null) {
|
|
|
|
errMsg = 'Malformed private key (expected DSA y value)';
|
|
|
|
if (keyInfo._decrypted)
|
|
|
|
errMsg += '. Bad passphrase?';
|
|
|
|
throw new Error(errMsg);
|
|
|
|
}
|
|
|
|
|
|
|
|
publicKey = new Buffer(4 + 7 // ssh-dss
|
|
|
|
+ 4 + p.length
|
|
|
|
+ 4 + q.length
|
|
|
|
+ 4 + g.length
|
|
|
|
+ 4 + y.length);
|
|
|
|
|
|
|
|
publicKey.writeUInt32BE(7, 0, true);
|
|
|
|
publicKey.write('ssh-dss', 4, 7, 'ascii');
|
|
|
|
|
|
|
|
i = 4 + 7;
|
|
|
|
publicKey.writeUInt32BE(p.length, i, true);
|
|
|
|
p.copy(publicKey, i += 4);
|
|
|
|
|
|
|
|
publicKey.writeUInt32BE(q.length, i += p.length, true);
|
|
|
|
q.copy(publicKey, i += 4);
|
|
|
|
|
|
|
|
publicKey.writeUInt32BE(g.length, i += q.length, true);
|
|
|
|
g.copy(publicKey, i += 4);
|
|
|
|
|
|
|
|
publicKey.writeUInt32BE(y.length, i += g.length, true);
|
|
|
|
y.copy(publicKey, i += 4);
|
|
|
|
} else { // ECDSA
|
|
|
|
d = asnReader.readString(Ber.OctetString, true);
|
|
|
|
if (d === null)
|
|
|
|
throw new Error('Malformed private key (expected ECDSA private key)');
|
|
|
|
asnReader.readByte(); // Skip "complex" context type byte
|
|
|
|
var offset = asnReader.readLength(); // Skip context length
|
|
|
|
if (offset === null)
|
|
|
|
throw new Error('Malformed private key (expected ECDSA context value)');
|
|
|
|
asnReader._offset = offset;
|
|
|
|
ecCurveOID = asnReader.readOID();
|
|
|
|
if (ecCurveOID === null)
|
|
|
|
throw new Error('Malformed private key (expected ECDSA curve)');
|
|
|
|
var tempECDH;
|
|
|
|
switch (ecCurveOID) {
|
|
|
|
case '1.2.840.10045.3.1.7':
|
|
|
|
// prime256v1/secp256r1
|
|
|
|
keyInfo.curve = ecCurveName = 'nistp256';
|
|
|
|
tempECDH = crypto.createECDH('prime256v1');
|
|
|
|
break;
|
|
|
|
case '1.3.132.0.34':
|
|
|
|
// secp384r1
|
|
|
|
keyInfo.curve = ecCurveName = 'nistp384';
|
|
|
|
tempECDH = crypto.createECDH('secp384r1');
|
|
|
|
break;
|
|
|
|
case '1.3.132.0.35':
|
|
|
|
// secp521r1
|
|
|
|
keyInfo.curve = ecCurveName = 'nistp521';
|
|
|
|
tempECDH = crypto.createECDH('secp521r1');
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Error('Malformed private key (unsupported EC curve)');
|
|
|
|
}
|
|
|
|
tempECDH.setPrivateKey(d);
|
|
|
|
Q = tempECDH.getPublicKey();
|
|
|
|
|
|
|
|
publicKey = new Buffer(4 + 19 // ecdsa-sha2-<curve name>
|
|
|
|
+ 4 + 8 // <curve name>
|
|
|
|
+ 4 + Q.length);
|
|
|
|
|
|
|
|
publicKey.writeUInt32BE(19, 0, true);
|
|
|
|
publicKey.write('ecdsa-sha2-' + ecCurveName, 4, 19, 'ascii');
|
|
|
|
|
|
|
|
publicKey.writeUInt32BE(8, 23, true);
|
|
|
|
publicKey.write(ecCurveName, 27, 8, 'ascii');
|
|
|
|
|
|
|
|
publicKey.writeUInt32BE(Q.length, 35, true);
|
|
|
|
Q.copy(publicKey, 39);
|
|
|
|
}
|
|
|
|
} else if (keyInfo.public) {
|
|
|
|
publicKey = keyInfo.public;
|
|
|
|
if (keyInfo.type === 'ec') {
|
|
|
|
// TODO: support adding ecdsa-* prefix
|
|
|
|
ecCurveName = keyInfo.curve;
|
|
|
|
} else if (publicKey[0] !== 0
|
|
|
|
// check for missing ssh-{dsa,rsa} prefix
|
|
|
|
|| publicKey[1] !== 0
|
|
|
|
|| publicKey[2] !== 0
|
|
|
|
|| publicKey[3] !== 7
|
|
|
|
|| publicKey[4] !== 115
|
|
|
|
|| publicKey[5] !== 115
|
|
|
|
|| publicKey[6] !== 104
|
|
|
|
|| publicKey[7] !== 45
|
|
|
|
|| ((publicKey[8] !== 114
|
|
|
|
|| publicKey[9] !== 115
|
|
|
|
|| publicKey[10] !== 97)
|
|
|
|
&&
|
|
|
|
((publicKey[8] !== 100
|
|
|
|
|| publicKey[9] !== 115
|
|
|
|
|| publicKey[10] !== 115)))) {
|
|
|
|
var newPK = new Buffer(4 + 7 + publicKey.length);
|
|
|
|
publicKey.copy(newPK, 11);
|
|
|
|
newPK.writeUInt32BE(7, 0, true);
|
|
|
|
if (keyInfo.type === 'rsa')
|
|
|
|
newPK.write('ssh-rsa', 4, 7, 'ascii');
|
|
|
|
else
|
|
|
|
newPK.write('ssh-dss', 4, 7, 'ascii');
|
|
|
|
publicKey = newPK;
|
|
|
|
}
|
|
|
|
} else
|
|
|
|
throw new Error('Missing data generated by parseKey()');
|
|
|
|
|
|
|
|
// generate a public key format for use with OpenSSL
|
|
|
|
|
|
|
|
i = 4 + 7;
|
|
|
|
|
|
|
|
var fulltype;
|
|
|
|
var asn1KeyBuf;
|
|
|
|
if (keyInfo.type === 'rsa') {
|
|
|
|
fulltype = 'ssh-rsa';
|
|
|
|
asn1KeyBuf = RSAKeySSHToASN1(publicKey.slice(4 + 7));
|
|
|
|
} else if (keyInfo.type === 'dss') {
|
|
|
|
fulltype = 'ssh-dss';
|
|
|
|
asn1KeyBuf = DSAKeySSHToASN1(publicKey.slice(4 + 7));
|
|
|
|
} else { // ECDSA
|
|
|
|
fulltype = 'ecdsa-sha2-' + ecCurveName;
|
|
|
|
asn1KeyBuf = ECDSAKeySSHToASN1(publicKey.slice(4 + 19));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!asn1KeyBuf)
|
|
|
|
throw new Error('Invalid SSH-formatted public key');
|
|
|
|
|
|
|
|
var b64key = asn1KeyBuf.toString('base64').replace(RE_KEY_LEN, '$1\n');
|
|
|
|
var fullkey = '-----BEGIN PUBLIC KEY-----\n'
|
|
|
|
+ b64key
|
|
|
|
+ (b64key[b64key.length - 1] === '\n' ? '' : '\n')
|
|
|
|
+ '-----END PUBLIC KEY-----';
|
|
|
|
|
|
|
|
return {
|
|
|
|
type: keyInfo.type,
|
|
|
|
fulltype: fulltype,
|
|
|
|
curve: ecCurveName,
|
|
|
|
public: publicKey,
|
|
|
|
publicOrig: new Buffer(fullkey)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function verifyPPKMAC(keyInfo, passphrase, privateKey) {
|
|
|
|
if (keyInfo._macresult !== undefined)
|
|
|
|
return keyInfo._macresult;
|
|
|
|
else if (!keyInfo.ppk)
|
|
|
|
throw new Error("Key isn't a PPK");
|
|
|
|
else if (!keyInfo.privateMAC)
|
|
|
|
throw new Error('Missing MAC');
|
|
|
|
else if (!privateKey)
|
|
|
|
throw new Error('Missing raw private key data');
|
|
|
|
else if (keyInfo.encryption && typeof passphrase !== 'string')
|
|
|
|
throw new Error('Missing passphrase for encrypted PPK');
|
|
|
|
else if (keyInfo.encryption && !keyInfo._decrypted)
|
|
|
|
throw new Error('PPK must be decrypted before verifying MAC');
|
|
|
|
|
|
|
|
var mac = keyInfo.privateMAC;
|
|
|
|
var typelen = keyInfo.fulltype.length;
|
|
|
|
// encryption algorithm is converted at this point for use with OpenSSL,
|
|
|
|
// so we need to use the original value so that the MAC is calculated
|
|
|
|
// correctly
|
|
|
|
var enc = (keyInfo.encryption ? 'aes256-cbc' : 'none');
|
|
|
|
var enclen = enc.length;
|
|
|
|
var commlen = Buffer.byteLength(keyInfo.comment);
|
|
|
|
var pub = keyInfo.public;
|
|
|
|
var publen = pub.length;
|
|
|
|
var privlen = privateKey.length;
|
|
|
|
var macdata = new Buffer(4 + typelen
|
|
|
|
+ 4 + enclen
|
|
|
|
+ 4 + commlen
|
|
|
|
+ 4 + publen
|
|
|
|
+ 4 + privlen);
|
|
|
|
var p = 0;
|
|
|
|
|
|
|
|
macdata.writeUInt32BE(typelen, p, true);
|
|
|
|
macdata.write(keyInfo.fulltype, p += 4, typelen, 'ascii');
|
|
|
|
macdata.writeUInt32BE(enclen, p += typelen, true);
|
|
|
|
macdata.write(enc, p += 4, enclen, 'ascii');
|
|
|
|
macdata.writeUInt32BE(commlen, p += enclen, true);
|
|
|
|
macdata.write(keyInfo.comment, p += 4, commlen, 'utf8');
|
|
|
|
macdata.writeUInt32BE(publen, p += commlen, true);
|
|
|
|
pub.copy(macdata, p += 4);
|
|
|
|
macdata.writeUInt32BE(privlen, p += publen, true);
|
|
|
|
privateKey.copy(macdata, p += 4);
|
|
|
|
|
|
|
|
if (typeof passphrase !== 'string')
|
|
|
|
passphrase = '';
|
|
|
|
|
|
|
|
var mackey = crypto.createHash('sha1')
|
|
|
|
.update('putty-private-key-file-mac-key', 'ascii')
|
|
|
|
.update(passphrase, 'utf8')
|
|
|
|
.digest();
|
|
|
|
|
|
|
|
var calcMAC = crypto.createHmac('sha1', mackey)
|
|
|
|
.update(macdata)
|
|
|
|
.digest('hex');
|
|
|
|
|
|
|
|
return (keyInfo._macresult = (calcMAC === mac));
|
|
|
|
}
|
|
|
|
|
|
|
|
function convertPPKPrivate(keyInfo) {
|
|
|
|
if (!keyInfo.ppk || !keyInfo.public || !keyInfo.private)
|
|
|
|
throw new Error("Key isn't a PPK");
|
|
|
|
else if (keyInfo._converted)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
var pub = keyInfo.public;
|
|
|
|
var priv = keyInfo.private;
|
|
|
|
var asnWriter = new Ber.Writer();
|
|
|
|
var p;
|
|
|
|
var q;
|
|
|
|
|
|
|
|
if (keyInfo.type === 'rsa') {
|
|
|
|
var e = readString(pub, 4 + 7);
|
|
|
|
var n = readString(pub, pub._pos);
|
|
|
|
var d = readString(priv, 0);
|
|
|
|
p = readString(priv, priv._pos);
|
|
|
|
q = readString(priv, priv._pos);
|
|
|
|
var iqmp = readString(priv, priv._pos);
|
|
|
|
var p1 = new BigInteger(p, 256);
|
|
|
|
var q1 = new BigInteger(q, 256);
|
|
|
|
var dmp1 = new BigInteger(d, 256);
|
|
|
|
var dmq1 = new BigInteger(d, 256);
|
|
|
|
|
|
|
|
dmp1 = new Buffer(dmp1.mod(p1.subtract(BigInteger.ONE)).toByteArray());
|
|
|
|
dmq1 = new Buffer(dmq1.mod(q1.subtract(BigInteger.ONE)).toByteArray());
|
|
|
|
|
|
|
|
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();
|
|
|
|
} else {
|
|
|
|
p = readString(pub, 4 + 7);
|
|
|
|
q = readString(pub, pub._pos);
|
|
|
|
var g = readString(pub, pub._pos);
|
|
|
|
var y = readString(pub, pub._pos);
|
|
|
|
var x = readString(priv, 0);
|
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
|
|
|
var b64key = asnWriter.buffer.toString('base64').replace(RE_KEY_LEN, '$1\n');
|
|
|
|
var fullkey = '-----BEGIN '
|
|
|
|
+ (keyInfo.type === 'rsa' ? 'RSA' : 'DSA')
|
|
|
|
+ ' PRIVATE KEY-----\n'
|
|
|
|
+ b64key
|
|
|
|
+ (b64key[b64key.length - 1] === '\n' ? '' : '\n')
|
|
|
|
+ '-----END '
|
|
|
|
+ (keyInfo.type === 'rsa' ? 'RSA' : 'DSA')
|
|
|
|
+ ' PRIVATE KEY-----';
|
|
|
|
|
|
|
|
keyInfo.private = asnWriter.buffer;
|
|
|
|
keyInfo.privateOrig = new Buffer(fullkey);
|
|
|
|
keyInfo._converted = true;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
function readString(buffer, start, encoding, stream, cb, maxLen) {
|
|
|
|
if (encoding && !Buffer.isBuffer(encoding) && typeof encoding !== 'string') {
|
|
|
|
if (typeof cb === 'number')
|
|
|
|
maxLen = cb;
|
|
|
|
cb = stream;
|
|
|
|
stream = encoding;
|
|
|
|
encoding = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
start || (start = 0);
|
|
|
|
var bufferLen = buffer.length;
|
|
|
|
var left = (bufferLen - start);
|
|
|
|
var len;
|
|
|
|
var end;
|
|
|
|
if (start < 0 || start >= bufferLen || left < 4) {
|
|
|
|
stream && stream._cleanup(cb);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
len = buffer.readUInt32BE(start, true);
|
|
|
|
if (len > (maxLen || MAX_STRING_LEN) || left < (4 + len)) {
|
|
|
|
stream && stream._cleanup(cb);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
start += 4;
|
|
|
|
end = start + len;
|
|
|
|
buffer._pos = end;
|
|
|
|
|
|
|
|
if (encoding) {
|
|
|
|
if (Buffer.isBuffer(encoding)) {
|
|
|
|
buffer.copy(encoding, 0, start, end);
|
|
|
|
return encoding;
|
|
|
|
} else
|
|
|
|
return buffer.toString(encoding, start, end);
|
|
|
|
} else
|
|
|
|
return buffer.slice(start, end);
|
|
|
|
}
|
|
|
|
|