Compare commits

...

7 Commits

15 changed files with 366 additions and 58 deletions

View File

@ -5,19 +5,23 @@ and convert it into a public JWK.
Works for RSA and ECDSA public keys.
Features
========
# Features
&lt; 100 lines of code | <1kb gzipped | 1.8kb minified | 3.1kb with comments
* [x] SSH Public Keys
* [x] RSA Public Keys
* [x] SSH Public Keys ([RFC 4253](https://coolaj86.com/articles/the-ssh-public-key-format/))
* fingerprint
* [x] OpenSSH Private Keys
* [x] RSA
* 2048, 3072, 4096
* [x] EC Public Keys
* P-256 (prime256v1, secp256r1)
* P-384 (secp384r1)
* [x] Browser Version
* [Bluecrypt SSH to JWK](https://git.coolaj86.com/coolaj86/bluecrypt-ssh-to-jwk.js)
Note: Lines of code have increased by about 2x since adding private key support.
### Need JWK to SSH? SSH to PEM?
Try one of these:
@ -28,9 +32,12 @@ Try one of these:
### Need SSH Private Keys?
SSH private keys are just normal PEM files,
Many SSH private keys are just normal PEM files,
so you can use Eckles or Rasha, as mentioned above.
As for the [OpenSSH-specific Private Keys](https://coolaj86.com/articles/the-openssh-private-key-format/),
both EC and RSA are fully supported.
# CLI
You can install `ssh-to-jwk` and use it from command line:
@ -43,22 +50,43 @@ npm install -g ssh-to-jwk
ssh-to-jwk ~/.ssh/id_rsa.pub
```
```bash
ssh-to-jwk ~/.ssh/id_rsa
```
# Usage
You can also use it from JavaScript:
**SSH to JWK**
```js
var fs = require('fs');
var sshtojwk = require('ssh-to-jwk');
var ssh;
var pub = fs.readFileSync("./id_rsa.pub");
var ssh = sshtojwk.parse(pub);
ssh = sshtojwk.parse({ pub: fs.readFileSync("./id_rsa.pub") });
console.info(ssh.jwk);
// For OpenSSH PEMs only, use Rasha for standard RSA or Eckles for standard EC
ssh = sshtojwk.parse({ pem: fs.readFileSync("./id_rsa") });
console.info(ssh.jwk);
```
Legal
-----
**SSH Fingerprint**
```js
var fs = require('fs');
var sshtojwk = require('ssh-to-jwk');
var pub = fs.readFileSync("./id_rsa.pub");
sshtojwk.fingerprint({ pub: pub }).then(function (fingerprint) {
console.info(fingerprint);
// SHA256:yCB62vBVsOwqksgYwy/WDbaMF2PhPijAwcrlzmrxfko
});
```
# Legal
[ssh-to-jwk.js](https://git.coolaj86.com/coolaj86/ssh-to-jwk.js) |
MPL-2.0 |

View File

@ -6,12 +6,27 @@ var path = require('path');
var sshtojwk = require('../index.js');
var pubfile = process.argv[2];
var pub = process.argv[3];
if (!pubfile) {
pubfile = path.join(require('os').homedir(), '.ssh/id_rsa.pub');
}
var buf = fs.readFileSync(pubfile);
var ssh = sshtojwk.parse(buf.toString('ascii'));
var txt = buf.toString('ascii');
var opts = { public: 'public' === pub };
var ssh;
console.info(JSON.stringify(ssh.jwk, null, 2));
if ('-' === txt[0]) {
opts.pem = txt;
} else {
opts.pub = txt;
}
ssh = sshtojwk.parse(opts);
// Finally! https://superuser.com/a/714195
sshtojwk.fingerprint(ssh).then(function (fingerprint) {
console.warn('The key fingerprint is:\n' + fingerprint + ' ' + ssh.comment);
console.info(JSON.stringify(ssh.jwk, null, 2));
});

View File

@ -0,0 +1,7 @@
{
"kty": "EC",
"crv": "P-256",
"d": "iYydo27aNGO9DBUWeGEPD8oNi1LZDqfxPmQlieLBjVQ",
"x": "IT1SWLxsacPiE5Z16jkopAn8_-85rMjgyCokrnjDft4",
"y": "mP2JwOAOdMmXuwpxbKng3KZz27mz-nKWIlXJ3rzSGMo"
}

View File

@ -0,0 +1,9 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQQhPVJYvGxpw+ITlnXqOSikCfz/7zms
yODIKiSueMN+3pj9icDgDnTJl7sKcWyp4Nymc9u5s/pyliJVyd680hjKAAAAqGJjanNiY2
pzAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCE9Uli8bGnD4hOW
deo5KKQJ/P/vOazI4MgqJK54w37emP2JwOAOdMmXuwpxbKng3KZz27mz+nKWIlXJ3rzSGM
oAAAAhAImMnaNu2jRjvQwVFnhhDw/KDYtS2Q6n8T5kJYniwY1UAAAADnJvb3RAbG9jYWxo
b3N0AQ==
-----END OPENSSH PRIVATE KEY-----

View File

@ -0,0 +1,7 @@
{
"kty": "EC",
"crv": "P-384",
"d": "XlyuCEWSTTS8U79O_Mz05z18vh4kb10szvu_7pdXuGWV6lfEyPExyUYWsA6A2kdV",
"x": "2zEU0bKCa7ejKLIJ8oPGnLhqhxyiv4_w38K2a0SPC6dsSd9_glNJ8lcqv0sff5Gb",
"y": "VD4jnu83S6scn6_TeAj3EZOREGbOs6dzoVpaugn-XQMMyC9O4VLbDDFGBZTJlMsb"
}

View File

@ -0,0 +1,10 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS
1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQTbMRTRsoJrt6Mosgnyg8acuGqHHKK/
j/DfwrZrRI8Lp2xJ33+CU0nyVyq/Sx9/kZtUPiOe7zdLqxyfr9N4CPcRk5EQZs6zp3OhWl
q6Cf5dAwzIL07hUtsMMUYFlMmUyxsAAADYYmNqc2JjanMAAAATZWNkc2Etc2hhMi1uaXN0
cDM4NAAAAAhuaXN0cDM4NAAAAGEE2zEU0bKCa7ejKLIJ8oPGnLhqhxyiv4/w38K2a0SPC6
dsSd9/glNJ8lcqv0sff5GbVD4jnu83S6scn6/TeAj3EZOREGbOs6dzoVpaugn+XQMMyC9O
4VLbDDFGBZTJlMsbAAAAMF5crghFkk00vFO/TvzM9Oc9fL4eJG9dLM77v+6XV7hllepXxM
jxMclGFrAOgNpHVQAAAA5yb290QGxvY2FsaG9zdAEC
-----END OPENSSH PRIVATE KEY-----

View File

@ -0,0 +1,11 @@
{
"kty": "RSA",
"n": "m2ttVBxPlWw06ZmGBWVDlfjkPAJ4DgnY0TrDwtCohHzLxGhDNzUJefLukC-xu0LBKylYojT5vTkxaOhxeSYo31syu4WhxbkTBLICOFcCGMob6pSQ38P8LdAIlb0pqDHxEJ9adWomjuFf0SUhN1cP7s9m8Yk9trkpEqjskocn2BOnTB57qAZM6-I70on0_iDZm7-jcqOPgADAmbWHhy67BXkk4yy_YzD4yOGZFXZcNp915_TW5bRd__AKPHUHxJasPiyEFqlNKBR2DSD-LbX5eTmzCh2ikrwTMja7mUdBJf2bK3By5AB0Qi49OykUCfNZeQlEz7UNNj9RGps_50-CNw",
"e": "AQAB",
"d": "Cpfo7Mm9Nu8YMC_xrZ54W9mKHPkCG9rZ93Ds9PNp-RXUgb-ljTbFPZWsYxGNKLllFz8LNosr1pT2ZDMrwNk0Af1iWNvD6gkyXaiQdCyiDPSBsJyNv2LJZon-e85X74nv53UlIkmo9SYxdLz2JaJ-iIWEe8Qh-7llLktrTJV_xr98_tbhgSppz_IeOymq3SEZaQHM8pTU7w7XvCj2pb9r8fN0M0XcgWZIaf3LGEfkhF_WtX67XJ0C6-LbkT51jtlLRNGX6haGdscXS0OWWjKOJzKGuV-NbthEn5rmRtVnjRZ3yaxQ0ud8vC-NONn7yvGUlOur1IdDzJ_YfHPt9sHMQQ",
"p": "ynG-t9HwKCN3MWRYFdnFzi9-02Qcy3p8B5pu3ary2E70hYn2pHlUG2a9BNE8c5xHQ3Hx43WoWf6s0zOunPV1G28LkU_UYEbAtPv_PxSmzpQp9n9XnYvBLBF8Y3z7gxgLn1vVFNARrQdRtj87qY3aw7E9S4DsGcAarIuOT2TsTCE",
"q": "xIkAjgUzB1zaUzJtW2Zgvp9cYYr1DmpH30ePZl3c_8397_DZDDo46fnFYjs6uPa03HpmKUnbjwr14QHlfXlntJBEuXxcqLjkdKdJ4ob7xueLTK4suo9V8LSrkLChVxlZQwnFD2E5ll0sVeeDeMJHQw38ahSrBFEVnxjpnPh1Q1c",
"dp": "tzDGjECFOU0ehqtuqhcuT63a7h8hj19-7MJqoFwY9HQ-ALkfXyYLXeBSGxHbyiIYuodZg6LsfMNgUJ3r3Eyhc_nAVfYPEC_2IdAG4WYmq7iXYF9LQV09qEsKbFykm7QekE3hO7wswo5k-q2tp3ieBYdVGAXJoGOdv5VpaZ7B1QE",
"dq": "kh5dyDk7YCz7sUFbpsmuAeuPjoH2ghooh2u3xN7iUVmAg-ToKjwbVnG5-7eXiC779rQVwnrD_0yh1AFJ8wjRPqDIR7ObXGHikIxT1VSQWqiJm6AfZzDsL0LUD4YS3iPdhob7-NxLKWzqao_u4lhnDQaX9PKa12HFlny6K1daL48",
"qi": "AlHWbx1gp6Z9pbw_1hlS7HuXAgWoX7IjbTUelldf4gkriDWLOrj3QCZcO4ZvZvEwJhVlsny9LO8IkbwGJEL6cXraK08ByVS2mwQyflgTgGNnpzixyEUL_mrQLx6y145FHcxfeqNInMhep-0Mxn1D5nlhmIOgRApS0t9VoXtHhFU"
}

View File

@ -0,0 +1,27 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEAm2ttVBxPlWw06ZmGBWVDlfjkPAJ4DgnY0TrDwtCohHzLxGhDNzUJ
efLukC+xu0LBKylYojT5vTkxaOhxeSYo31syu4WhxbkTBLICOFcCGMob6pSQ38P8LdAIlb
0pqDHxEJ9adWomjuFf0SUhN1cP7s9m8Yk9trkpEqjskocn2BOnTB57qAZM6+I70on0/iDZ
m7+jcqOPgADAmbWHhy67BXkk4yy/YzD4yOGZFXZcNp915/TW5bRd//AKPHUHxJasPiyEFq
lNKBR2DSD+LbX5eTmzCh2ikrwTMja7mUdBJf2bK3By5AB0Qi49OykUCfNZeQlEz7UNNj9R
Gps/50+CNwAAA8hiY2pzYmNqcwAAAAdzc2gtcnNhAAABAQCba21UHE+VbDTpmYYFZUOV+O
Q8AngOCdjROsPC0KiEfMvEaEM3NQl58u6QL7G7QsErKViiNPm9OTFo6HF5JijfWzK7haHF
uRMEsgI4VwIYyhvqlJDfw/wt0AiVvSmoMfEQn1p1aiaO4V/RJSE3Vw/uz2bxiT22uSkSqO
yShyfYE6dMHnuoBkzr4jvSifT+INmbv6Nyo4+AAMCZtYeHLrsFeSTjLL9jMPjI4ZkVdlw2
n3Xn9NbltF3/8Ao8dQfElqw+LIQWqU0oFHYNIP4ttfl5ObMKHaKSvBMyNruZR0El/ZsrcH
LkAHRCLj07KRQJ81l5CUTPtQ02P1Eamz/nT4I3AAAAAwEAAQAAAQAKl+jsyb027xgwL/Gt
nnhb2Yoc+QIb2tn3cOz082n5FdSBv6WNNsU9laxjEY0ouWUXPws2iyvWlPZkMyvA2TQB/W
JY28PqCTJdqJB0LKIM9IGwnI2/Yslmif57zlfvie/ndSUiSaj1JjF0vPYlon6IhYR7xCH7
uWUuS2tMlX/Gv3z+1uGBKmnP8h47KardIRlpAczylNTvDte8KPalv2vx83QzRdyBZkhp/c
sYR+SEX9a1frtcnQLr4tuRPnWO2UtE0ZfqFoZ2xxdLQ5ZaMo4nMoa5X41u2ESfmuZG1WeN
FnfJrFDS53y8L4042fvK8ZSU66vUh0PMn9h8c+32wcxBAAAAgAJR1m8dYKemfaW8P9YZUu
x7lwIFqF+yI201HpZXX+IJK4g1izq490AmXDuGb2bxMCYVZbJ8vSzvCJG8BiRC+nF62itP
AclUtpsEMn5YE4BjZ6c4schFC/5q0C8esteORR3MX3qjSJzIXqftDMZ9Q+Z5YZiDoEQKUt
LfVaF7R4RVAAAAgQDKcb630fAoI3cxZFgV2cXOL37TZBzLenwHmm7dqvLYTvSFifakeVQb
Zr0E0TxznEdDcfHjdahZ/qzTM66c9XUbbwuRT9RgRsC0+/8/FKbOlCn2f1edi8EsEXxjfP
uDGAufW9UU0BGtB1G2PzupjdrDsT1LgOwZwBqsi45PZOxMIQAAAIEAxIkAjgUzB1zaUzJt
W2Zgvp9cYYr1DmpH30ePZl3c/8397/DZDDo46fnFYjs6uPa03HpmKUnbjwr14QHlfXlntJ
BEuXxcqLjkdKdJ4ob7xueLTK4suo9V8LSrkLChVxlZQwnFD2E5ll0sVeeDeMJHQw38ahSr
BFEVnxjpnPh1Q1cAAAAOcm9vdEBsb2NhbGhvc3QBAgMEBQ==
-----END OPENSSH PRIVATE KEY-----

View File

@ -0,0 +1 @@
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBH1Zk94FK+LGSGNA6H3IJdeUTHkI30zKH3gIknWuKB/zr3eBz3QeZqEhHmgwYfCWoyhLuQqtHD7gqnGqwXssa/E= aj@bowie.local

View File

@ -0,0 +1 @@
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBMM1wvONZSobkf6CtT9n5E06y+ofhXmpTVSWRTOpMiZi+bsKViBi1eptF4LMufUT4M5DDU3V/kT9iM8124uWSWwS/ernmTsENNOI2TlzXkKkJshXh5Z4tFIVIkEZAeW/6w== aj@bowie.local

View File

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDTNGUXvKQQ2sbdXdILzeI7u60ziBDHqCbrYxUkdro0LA8SqimkQexHKd9SBVvrxNm3l1LsCRmeIgrxlhwuptsygK80rRX5Jit2DCRP62tyMBIEIqLxAksMEW1z6M3XIdIj2jWkL6ei6Qver03JWt4TfxZG94axSPxPaHYf6Psb+lZnMfYrWXtVD9hJA6vQjKNqmpUo1I2dWq6e6F3Oepejfu9Cz1yMnEsNJG3COubJSXUkEdsJYSrW3wfUnMPBrlnjs8uP4NBTik+5SHmpb+pU3+2DjgC4tsnTIkiusaLIp/qJmS+X18pFzRCMy2jfX1QeHMPsXnBSvdfyIdZfIVs5 aj@bowie.local

View File

@ -10,6 +10,20 @@ Enc.base64ToBuf = function (str) {
return Buffer.from(str, 'base64');
};
Enc.base64ToHex = function (str) {
return Buffer.from(str, 'base64').toString('hex');
};
Enc.base64ToUrlBase64 = function (b64) {
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
};
Enc.bnToUrlBase64 = function (bn) {
var hex = bn.toString(16);
if (hex.length % 2) { hex = '0' + hex; }
return Enc.base64ToUrlBase64(Buffer.from(hex, 'hex').toString('base64'));
};
Enc.bufToBase64 = function (u8) {
return Buffer.from(u8).toString('base64');
};
@ -23,6 +37,5 @@ Enc.bufToHex = function (u8) {
};
Enc.bufToUrlBase64 = function (u8) {
return Enc.bufToBase64(u8)
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
return Enc.base64ToUrlBase64(Enc.bufToBase64(u8));
};

28
lib/pem.js Normal file
View File

@ -0,0 +1,28 @@
'use strict';
var PEM = module.exports;
var Enc = require('./encoding.js');
PEM.parseBlock = function pemToDer(pem) {
var lines = pem.trim().split(/\n/);
var end = lines.length - 1;
var head = lines[0].match(/-----BEGIN (.*)-----/);
var foot = lines[end].match(/-----END (.*)-----/);
if (head) {
lines = lines.slice(1, end);
head = head[1];
if (head !== foot[1]) {
throw new Error("headers and footers do not match");
}
}
return { type: head, bytes: Enc.base64ToBuf(lines.join('')) };
};
PEM.packBlock = function (opts) {
return '-----BEGIN ' + opts.type + '-----\n'
+ Enc.bufToBase64(opts.bytes).match(/.{1,64}/g).join('\n') + '\n'
+ '-----END ' + opts.type + '-----'
;
};

View File

@ -1,57 +1,126 @@
'use strict';
/*global BigInt*/
var SSH = module.exports;
var Enc = require('./encoding.js');
var PEM = require('./pem.js');
var bnwarn = false;
SSH.parse = function (ssh) {
ssh = ssh.split(/\s+/g);
var result = { type: ssh[0], jwk: null, comment: ssh[2] || '' };
var buf = Enc.base64ToBuf(ssh[1]);
var els = SSH.parseElements({ bytes: buf }).elements;
var typ = Enc.bufToBin(els[0]);
var len;
// RSA keys are all the same
if (SSH.types.rsa === typ) {
result.jwk = {
kty: 'RSA'
, n: Enc.bufToUrlBase64(els[2])
, e: Enc.bufToUrlBase64(els[1])
};
return result;
}
// EC keys are each different
if (SSH.types.p256 === typ) {
len = 32;
result.jwk = { kty: 'EC', crv: 'P-256' };
} else if (SSH.types.p384 === typ) {
len = 48;
result.jwk = { kty: 'EC', crv: 'P-384' };
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 {
throw new Error("Unsupported ssh public key type: "
+ Enc.bufToBin(els[0]));
ssh.elements = SSH.parseElements(ssh.bytes);
}
// 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);
// I don't think EC keys use 0x00 padding, but just in case
if (0x00 === x[0]) { x = x.slice(1); }
if (0x00 === y[0]) { y = y.slice(1); }
result.jwk.x = Enc.bufToUrlBase64(x);
result.jwk.y = Enc.bufToUrlBase64(y);
return result;
//delete ssh.bytes;
return SSH.parsePublicKey(ssh);
};
SSH.parseElements = function (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)
@ -65,6 +134,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]) {
@ -74,12 +144,90 @@ SSH.parseElements = function (ssh) {
index += len;
}
if (fulllen !== index) {
throw new Error(els.map(function (b) {
throw new Error("invalid ssh public key length \n" + els.map(function (b) {
return Enc.bufToHex(b);
}).join('\n') + "invalid ssh public key length");
}).join('\n'));
}
ssh.elements = els;
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 {
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: 0
, dq: 0
, qi: Enc.bufToUrlBase64(els[4])
};
if ('undefined' !== typeof BigInt) {
// BigInt doesn't use new
/*jshint newcap: false*/
// d mod (p - 1)
ssh.jwk.dp = Enc.bnToUrlBase64(BigInt('0x' + Enc.base64ToHex(ssh.jwk.d))
% (BigInt('0x' + Enc.base64ToHex(ssh.jwk.p)) - BigInt(1)));
ssh.jwk.dq = Enc.bnToUrlBase64(BigInt('0x' + Enc.base64ToHex(ssh.jwk.d))
% (BigInt('0x' + Enc.base64ToHex(ssh.jwk.q)) - BigInt(1)));
} else {
if (!bnwarn) {
bnwarn = true;
// TODO maybe conditionally bring in BigInt polyfill?
console.warn("ssh-to-jwk.js: Your version of node is outdated doesn't support BigInt");
console.log("JWKs will be missing `dp` and `dq` values. Update or use a BigInt polyfill.");
}
delete ssh.jwk.dp;
delete ssh.jwk.dq;
}
}
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;
};

View File

@ -1,6 +1,6 @@
{
"name": "ssh-to-jwk",
"version": "1.0.1",
"version": "1.2.6",
"description": "💯 SSH to JWK in a lightweight, zero-dependency library.",
"homepage": "https://git.coolaj86.com/coolaj86/ssh-to-jwk.js",
"main": "index.js",
@ -29,6 +29,8 @@
"RSA",
"EC",
"SSH",
"OpenSSH",
"fingerprint",
"JWK",
"ECDSA"
],