|
|
@ -2,11 +2,20 @@ |
|
|
|
|
|
|
|
var SSH = module.exports; |
|
|
|
var Enc = require('./encoding.js'); |
|
|
|
var PEM = require('./pem.js'); |
|
|
|
|
|
|
|
SSH.parse = function (opts) { |
|
|
|
var pub = opts.pub || opts; |
|
|
|
var ssh = SSH.parseBlock(pub); |
|
|
|
ssh = SSH.parseElements(ssh); |
|
|
|
if ('OPENSSH PRIVATE KEY' === ssh.type) { |
|
|
|
ssh = SSH.parsePrivateElements(ssh); |
|
|
|
if (opts.public) { |
|
|
|
ssh.elements = ssh.elements.slice(0, 3); |
|
|
|
} |
|
|
|
} else { |
|
|
|
ssh.elements = SSH.parseElements(ssh.bytes); |
|
|
|
} |
|
|
|
|
|
|
|
//delete ssh.bytes;
|
|
|
|
return SSH.parsePublicKey(ssh); |
|
|
|
}; |
|
|
@ -27,6 +36,9 @@ SSH.fingerprint = function (opts) { |
|
|
|
}; |
|
|
|
|
|
|
|
SSH.parseBlock = function (ssh) { |
|
|
|
if (/^-----BEGIN OPENSSH PRIVATE KEY-----/.test(ssh)) { |
|
|
|
return PEM.parseBlock(ssh); |
|
|
|
} |
|
|
|
ssh = ssh.split(/\s+/g); |
|
|
|
|
|
|
|
return { |
|
|
@ -36,10 +48,69 @@ SSH.parseBlock = function (ssh) { |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
SSH.parseElements = function (ssh) { |
|
|
|
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; |
|
|
|
|
|
|
|
// 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; |
|
|
|
|
|
|
|
// 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()); |
|
|
|
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)
|
|
|
@ -53,6 +124,7 @@ SSH.parseElements = function (ssh) { |
|
|
|
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]) { |
|
|
@ -67,8 +139,7 @@ SSH.parseElements = function (ssh) { |
|
|
|
}).join('\n')); |
|
|
|
} |
|
|
|
|
|
|
|
ssh.elements = els; |
|
|
|
return ssh; |
|
|
|
return els; |
|
|
|
}; |
|
|
|
|
|
|
|
SSH.parsePublicKey = function (ssh) { |
|
|
@ -78,11 +149,26 @@ SSH.parsePublicKey = function (ssh) { |
|
|
|
|
|
|
|
// RSA keys are all the same
|
|
|
|
if (SSH.types.rsa === typ) { |
|
|
|
ssh.jwk = { |
|
|
|
kty: 'RSA' |
|
|
|
, n: Enc.bufToUrlBase64(els[2]) |
|
|
|
, e: Enc.bufToUrlBase64(els[1]) |
|
|
|
}; |
|
|
|
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; |
|
|
|
} |
|
|
|
|
|
|
@ -101,11 +187,15 @@ SSH.parsePublicKey = function (ssh) { |
|
|
|
// 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]) { |
|
|
|
ssh.jwk.d = Enc.bufToUrlBase64(els[3]); |
|
|
|
} |
|
|
|
ssh.jwk.x = Enc.bufToUrlBase64(x); |
|
|
|
ssh.jwk.y = Enc.bufToUrlBase64(y); |
|
|
|
|
|
|
|