'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' };