💯 SSH to JWK 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.
 
 

232 lines
6.2 KiB

'use strict';
var SSH = module.exports;
var Enc = require('./encoding.js');
var PEM = require('./pem.js');
SSH.parse = function (opts) {
var pub = opts.pem || opts.pub || opts;
var ssh = SSH.parseBlock(pub);
if ('OPENSSH PRIVATE KEY' === ssh.type) {
ssh = SSH.parsePrivateElements(ssh);
if (7 === ssh.elements.length) {
// RSA Private Keys have the `e` and `n` swapped (which is actually more normal)
// but we have to reswap them to make them consistent with the public key format
ssh.elements.splice(1, 0, ssh.elements.splice(2 ,1)[0]);
}
if (opts.public) {
ssh.elements = ssh.elements.slice(0, 3);
}
} else {
ssh.elements = SSH.parseElements(ssh.bytes);
}
//delete ssh.bytes;
return SSH.parsePublicKey(ssh);
};
/*global Promise*/
SSH.fingerprint = function (opts) {
var ssh;
if (opts.bytes) {
ssh = opts;
} else {
ssh = SSH.parseBlock(opts.pub);
}
// for browser compat
return Promise.resolve().then(function () {
return 'SHA256:' + require('crypto').createHash('sha256')
.update(ssh.bytes).digest('base64').replace(/=+$/g, '');
});
};
SSH.parseBlock = function (ssh) {
if (/^-----BEGIN OPENSSH PRIVATE KEY-----/.test(ssh)) {
return PEM.parseBlock(ssh);
}
ssh = ssh.split(/\s+/g);
return {
type: ssh[0]
, bytes: Enc.base64ToBuf(ssh[1])
, comment: ssh[2]
};
};
SSH.parsePrivateElements = function (ssh) {
// https://coolaj86.com/articles/the-openssh-private-key-format/
var buf = ssh.bytes;
var fulllen = buf.byteLength || buf.length;
var offset = (buf.byteOffset || 0);
var dv = new DataView(buf.buffer.slice(offset, offset + fulllen));
var index = 0;
var padlen = 0;
var len;
var pub;
// The last byte will be either
// * a non-printable pad character
// * a printable comment character
function lastByteIsPad() {
var n = ssh.bytes[(ssh.bytes.bytesLength || ssh.bytes.length) - 1];
return n >= 0x01 && n <= 0x07;
}
while (lastByteIsPad()) {
padlen += 1;
len = (ssh.bytes.bytesLength || ssh.bytes.length);
ssh.bytes = ssh.bytes.slice(0, len - 1);
}
// o p e n s s h - k e y - v 1 NULL
// 6f 70 65 6e 73 73 68 2d 6b 65 79 2d 76 31 00
// 15 characters
// 4-byte len, "none" (encryption)
// 4-byte len, "none" (kdfname)
if ('none' !== Enc.bufToBin(ssh.bytes.slice(15 + 8 + 4, 15 + 8 + 8))) {
throw new Error("Key is either encrypted (not yet supported), corrupt, or not openssh-key-v1");
}
if (padlen >= 8) {
throw new Error("Padding length should be between 0 and 7, not '" + padlen + "'."
+ " Probably not an ssh private key.");
}
// 4-byte len, nil (kdf)
// 4-byte number of keys
index += 15 + 8 + 8 + 4 + 4;
// length of public key
len = dv.getUint32(index, false);
// throw away public key (it's in the private key)
index += 4 + len;
pub = ssh.bytes.slice(index - len, index);
// length of dummy checksum + private key + padding
len = dv.getUint32(index, false) - padlen;
// throw away dummy checksum
index += 4 + 8;
ssh.elements = SSH.parseElements(ssh.bytes.slice(index, index + (len - 8)));
index += Array.prototype.reduce.call(ssh.elements, function (el, sum) {
// 32-bit len + element len
return 4 + (el.byteLength || el.length) + sum;
}, 0);
// comment will exist, even if it's an empty string
ssh.comment = Enc.bufToBin(ssh.elements.pop());
ssh.bytes = pub;
return ssh;
};
SSH.parseElements = function (buf) {
var fulllen = buf.byteLength || buf.length;
// Note: node has weird offsets
var offset = (buf.byteOffset || 0);
var i = 0;
var index = 0;
// using dataview to be browser-compatible (I do want _some_ code reuse)
var dv = new DataView(buf.buffer.slice(offset, offset + fulllen));
var els = [];
var el;
var len;
while (index < fulllen) {
i += 1;
if (i > 15) { throw new Error("15+ elements, probably not a public ssh key"); }
len = dv.getUint32(index, false);
index += 4;
if (0 === len) { continue; }
el = buf.slice(index, index + len);
// remove BigUInt '00' prefix
if (0x00 === el[0]) {
el = el.slice(1);
}
els.push(el);
index += len;
}
if (fulllen !== index) {
throw new Error("invalid ssh public key length \n" + els.map(function (b) {
return Enc.bufToHex(b);
}).join('\n'));
}
return els;
};
SSH.parsePublicKey = function (ssh) {
var els = ssh.elements;
var typ = Enc.bufToBin(els[0]);
var len;
// RSA keys are all the same
if (SSH.types.rsa === typ) {
if (3 === els.length) {
ssh.jwk = {
kty: 'RSA'
, n: Enc.bufToUrlBase64(els[2])
, e: Enc.bufToUrlBase64(els[1])
};
} else {
console.log('len:', els.length);
ssh.jwk = {
kty: 'RSA'
, n: Enc.bufToUrlBase64(els[2])
, e: Enc.bufToUrlBase64(els[1])
, d: Enc.bufToUrlBase64(els[3])
, p: Enc.bufToUrlBase64(els[5])
, q: Enc.bufToUrlBase64(els[6])
//, dp: Enc.bufToUrlBase64(els[x])
//, dq: Enc.bufToUrlBase64(els[x])
, qi: Enc.bufToUrlBase64(els[4])
};
}
return ssh;
}
// EC keys are each different
if (SSH.types.p256 === typ) {
len = 32;
ssh.jwk = { kty: 'EC', crv: 'P-256' };
} else if (SSH.types.p384 === typ) {
len = 48;
ssh.jwk = { kty: 'EC', crv: 'P-384' };
} else {
throw new Error("Unsupported ssh public key type: "
+ Enc.bufToBin(els[0]));
}
// els[1] is just a repeat of a subset of els[0]
var x = els[2].slice(1, 1 + len);
var y = els[2].slice(1 + len, 1 + len + len);
var d;
// I don't think EC keys use 0x00 padding, but just in case
while (0x00 === x[0]) { x = x.slice(1); }
while (0x00 === y[0]) { y = y.slice(1); }
if (els[3]) {
d = els[3];
while (0x00 === d[0]) { d = d.slice(1); }
ssh.jwk.d = Enc.bufToUrlBase64(d);
}
ssh.jwk.x = Enc.bufToUrlBase64(x);
ssh.jwk.y = Enc.bufToUrlBase64(y);
return ssh;
};
SSH.types = {
// 19 '00000013'
// e c d s a - s h a 2 - n i s t p 2 5 6
// 65636473612d736861322d6e69737470323536
// 6e69737470323536
p256: 'ecdsa-sha2-nistp256'
// 19 '00000013'
// e c d s a - s h a 2 - n i s t p 3 8 4
// 65636473612d736861322d6e69737470333834
// 6e69737470323536
, p384: 'ecdsa-sha2-nistp384'
// 7 '00000007'
// s s h - r s a
// 7373682d727361
, rsa: 'ssh-rsa'
};