139 lines
3.8 KiB
JavaScript
139 lines
3.8 KiB
JavaScript
'use strict';
|
|
|
|
var Enc = require('./encoding.js');
|
|
var PEM = require('./pem.js');
|
|
var SSH = module.exports;
|
|
|
|
SSH.pack = function (opts) {
|
|
if (opts.jwk.d && !opts.public) {
|
|
return SSH._packPrivate(opts);
|
|
} else {
|
|
delete opts.jwk.d;
|
|
return SSH._packPublic(opts);
|
|
}
|
|
};
|
|
|
|
// https://tools.ietf.org/html/rfc4253#section-6.6
|
|
SSH._packPublic = function (opts) {
|
|
var els = SSH._packKey(opts);
|
|
var hex = SSH._packElements(els);
|
|
var typ = Enc.hexToBin(els[0]);
|
|
var parts = [ typ, Enc.hexToBase64(hex) ];
|
|
if (opts.comment) { parts.push(opts.comment); }
|
|
return parts.join(' ');
|
|
};
|
|
|
|
SSH._packPrivate = function (opts) {
|
|
var pubjwk = JSON.parse(JSON.stringify(opts.jwk));
|
|
delete pubjwk.d;
|
|
var pubels = SSH._packKey({ jwk: pubjwk });
|
|
var pubhex = SSH._packElements(pubels);
|
|
var els = SSH._packKey(opts);
|
|
var hex = SSH._packElements(els);
|
|
var privlen = hex.length/2;
|
|
var padlen = (privlen % 8) && (8 - (privlen % 8)) || 0; // blocksize is 8 (no cipher)
|
|
var bin = "openssh-key-v1" + String.fromCharCode(0)
|
|
+ Enc.hexToBin(
|
|
SSH._packElements([
|
|
Enc.binToHex("none") // ciphername
|
|
, Enc.binToHex("none") // kdfname
|
|
, "" // empty kdf
|
|
])
|
|
+ SSH._numToUint32Hex(1) // number of keys (always 1)
|
|
+ SSH._numToUint32Hex(pubhex.length/2) // pubkey length
|
|
+ pubhex
|
|
+ SSH._numToUint32Hex(8 + privlen + padlen) // privkey length
|
|
+ '62636a7362636a73' // 64-bit dummy checksum ("bcjs", "bcjs")
|
|
+ hex // (only cihpered keys use real checksums)
|
|
);
|
|
var pad = '';
|
|
var i;
|
|
for (i = 1; i <= padlen; i += 1) {
|
|
pad += '0' + i;
|
|
}
|
|
return PEM.packBlock({
|
|
type: "OPENSSH PRIVATE KEY"
|
|
, bytes: Enc.binToBuf(bin + Enc.hexToBin(pad))
|
|
});
|
|
};
|
|
|
|
SSH._packKey = function (opts) {
|
|
var jwk = opts.jwk;
|
|
var els = [];
|
|
var len;
|
|
|
|
if ("RSA" === jwk.kty) {
|
|
els.push(Enc.binToHex('ssh-rsa'));
|
|
if (jwk.d) {
|
|
// unswap n and e for private key format
|
|
els.push(SSH._padRsa(Enc.base64ToHex(jwk.n)));
|
|
els.push(SSH._padRsa(Enc.base64ToHex(jwk.e)));
|
|
els.push(SSH._padRsa(Enc.base64ToHex(jwk.d)));
|
|
els.push(SSH._padRsa(Enc.base64ToHex(jwk.qi)));
|
|
els.push(SSH._padRsa(Enc.base64ToHex(jwk.p)));
|
|
els.push(SSH._padRsa(Enc.base64ToHex(jwk.q)));
|
|
els.push(Enc.binToHex(opts.comment || ''));
|
|
} else {
|
|
// swap n and e for public key format
|
|
els.push(SSH._padRsa(Enc.base64ToHex(jwk.e)));
|
|
els.push(SSH._padRsa(Enc.base64ToHex(jwk.n)));
|
|
}
|
|
return els;
|
|
}
|
|
|
|
if ("P-256" === jwk.crv) {
|
|
els.push(Enc.binToHex('ecdsa-sha2-nistp256'));
|
|
els.push(Enc.binToHex('nistp256'));
|
|
len = 32;
|
|
} else if ("P-384" === jwk.crv) {
|
|
els.push(Enc.binToHex('ecdsa-sha2-nistp384'));
|
|
els.push(Enc.binToHex('nistp384'));
|
|
len = 48;
|
|
} else {
|
|
throw new Error("unknown key type " + (jwk.crv || jwk.kty));
|
|
}
|
|
|
|
els.push('04'
|
|
+ SSH._padEc(Enc.base64ToHex(jwk.x), len)
|
|
+ SSH._padEc(Enc.base64ToHex(jwk.y), len)
|
|
);
|
|
if (jwk.d) {
|
|
els.push(SSH._padEc(Enc.base64ToHex(jwk.d), len));
|
|
els.push(Enc.binToHex(opts.comment || ''));
|
|
}
|
|
|
|
return els;
|
|
};
|
|
|
|
SSH._packElements = function(els) {
|
|
return els.map(function (hex) {
|
|
return SSH._numToUint32Hex(hex.length/2) + hex;
|
|
}).join('');
|
|
};
|
|
|
|
SSH._numToUint32Hex = function (num) {
|
|
var hex = num.toString(16);
|
|
while (hex.length < 8) {
|
|
hex = '0' + hex;
|
|
}
|
|
return hex;
|
|
};
|
|
|
|
SSH._padRsa = function (hex) {
|
|
// BigInt is negative if the high order bit 0x80 is set,
|
|
// so ASN1, SSH, and many other formats pad with '0x00'
|
|
// to signifiy a positive number.
|
|
var i = parseInt(hex.slice(0, 2), 16);
|
|
if (0x80 & i) {
|
|
return '00' + hex;
|
|
}
|
|
return hex;
|
|
};
|
|
|
|
SSH._padEc = function (hex, len) {
|
|
while (hex.length < len * 2) {
|
|
hex = '00' + hex;
|
|
}
|
|
return hex;
|
|
};
|