💯 JWK to SSH in a lightweight, zero-dependency library.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

133 lines
3.6 KiB

'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'));
els.push(SSH._padRsa(Enc.base64ToHex(jwk.e)));
els.push(SSH._padRsa(Enc.base64ToHex(jwk.n)));
if (jwk.d) {
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 || ''));
}
return SSH._packElements(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;
};