v1.0.0: parse SSH public keys in the browser
This commit is contained in:
commit
86ee62e0d3
65
README.md
Normal file
65
README.md
Normal file
@ -0,0 +1,65 @@
|
||||
# Bluecrypt&trade SSH to JWK (for Browsers)
|
||||
|
||||
A minimal library to parse an SSH public key (`id_rsa.pub`)
|
||||
and convert it into a public JWK using Vanilla JS.
|
||||
|
||||
Works for RSA and ECDSA public keys.
|
||||
|
||||
# Features
|
||||
|
||||
< 150 lines of code | 1.0kb gzipped | 2.4kb minified | 3.7kb with comments
|
||||
|
||||
* [x] SSH Public Keys
|
||||
* [x] RSA Public Keys
|
||||
* [x] EC Public Keys
|
||||
* P-256 (prime256v1, secp256r1)
|
||||
* P-384 (secp384r1)
|
||||
* [x] node.js version
|
||||
* [ssh-to-jwk.js](https://git.coolaj86.com/coolaj86/ssh-to-jwk.js)
|
||||
|
||||
### Need SSH Private Keys?
|
||||
|
||||
SSH private keys (`id_rsa`) are just normal PEM files,
|
||||
so you can use Eckles or Rasha, as mentioned above.
|
||||
|
||||
# Web Demo
|
||||
|
||||
<https://coolaj86.com/demos/ssh-to-jwk/>
|
||||
|
||||
```bash
|
||||
git clone https://git.coolaj86.com/coolaj86/bluecrypt-ssh-to-jwk.js
|
||||
pushd bluecrypt-ssh-to-jwk.js/
|
||||
```
|
||||
|
||||
```bash
|
||||
open index.html
|
||||
```
|
||||
|
||||
# Usage
|
||||
|
||||
You can also use it as a library:
|
||||
|
||||
```html
|
||||
<script src="https://git.coolaj86.com/coolaj86/bluecrypt-ssh-to-jwk.js/raw/branch/master/ssh-to-jwk.js"></script>
|
||||
```
|
||||
|
||||
```js
|
||||
var pub = 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCE9Uli8bGnD4hOWdeo5KKQJ/P/vOazI4MgqJK54w37emP2JwOAOdMmXuwpxbKng3KZz27mz+nKWIlXJ3rzSGMo= root@localhost';
|
||||
|
||||
var ssh = SSH.parse(pub);
|
||||
|
||||
console.info(ssh.jwk);
|
||||
```
|
||||
|
||||
# Other Tools in the Bluecrypt Suite
|
||||
|
||||
* [Bluecrypt JWK to SSH](https://git.coolaj86.com/coolaj86/jwk-to-ssh.js) (RSA, EC, SSH)
|
||||
* [Bluecrypt ASN.1 decoder](https://git.coolaj86.com/coolaj86/asn1-parser.js) (x509, RSA, EC, etc)
|
||||
* [Bluecrypt ASN.1 builder](https://git.coolaj86.com/coolaj86/asn1-packer.js) (x509, RSA, EC, etc)
|
||||
|
||||
# Legal
|
||||
|
||||
[ssh-to-jwk.js](https://git.coolaj86.com/coolaj86/ssh-to-jwk.js) |
|
||||
MPL-2.0 |
|
||||
[Terms of Use](https://therootcompany.com/legal/#terms) |
|
||||
[Privacy Policy](https://therootcompany.com/legal/#privacy)
|
68
index.html
Normal file
68
index.html
Normal file
@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>SSH Pub Parser - Bluecrypt</title>
|
||||
<style>
|
||||
textarea {
|
||||
width: 42em;
|
||||
height: 10em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Bluecrypt SSH Public Key Parser</h1>
|
||||
|
||||
<textarea class="js-input" placeholder="Paste id_rsa.pub (or other SSH public key) here">ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCba21UHE+VbDTpmYYFZUOV+OQ8AngOCdjROsPC0KiEfMvEaEM3NQl58u6QL7G7QsErKViiNPm9OTFo6HF5JijfWzK7haHFuRMEsgI4VwIYyhvqlJDfw/wt0AiVvSmoMfEQn1p1aiaO4V/RJSE3Vw/uz2bxiT22uSkSqOyShyfYE6dMHnuoBkzr4jvSifT+INmbv6Nyo4+AAMCZtYeHLrsFeSTjLL9jMPjI4ZkVdlw2n3Xn9NbltF3/8Ao8dQfElqw+LIQWqU0oFHYNIP4ttfl5ObMKHaKSvBMyNruZR0El/ZsrcHLkAHRCLj07KRQJ81l5CUTPtQ02P1Eamz/nT4I3 root@localhost</textarea>
|
||||
|
||||
<pre><code class="js-hex"> </code></pre>
|
||||
|
||||
<pre><code class="js-jwk"> </code></pre>
|
||||
|
||||
<br>
|
||||
<p>Made with <a href="https://git.coolaj86.com/coolaj86/bluecrypt-ssh-to-jwk.js/">ssh-to-jwk.js</a></p>
|
||||
|
||||
<script src="./ssh-to-jwk.js"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
Enc.bufToHex = function toHex(u8) {
|
||||
var hex = [];
|
||||
var i, h;
|
||||
var len = (u8.byteLength || u8.length);
|
||||
|
||||
for (i = 0; i < len; i += 1) {
|
||||
h = u8[i].toString(16);
|
||||
if (h.length % 2) { h = '0' + h; }
|
||||
hex.push(h);
|
||||
}
|
||||
|
||||
return hex.join('').toLowerCase();
|
||||
};
|
||||
|
||||
var $input = document.querySelector('.js-input');
|
||||
|
||||
function convert() {
|
||||
console.log('keyup');
|
||||
var ssh;
|
||||
|
||||
try {
|
||||
var text = document.querySelector('.js-input').value.trim();
|
||||
ssh = SSH.parse(text);
|
||||
document.querySelector('.js-hex').innerText = ssh.elements.map(function (hex) {
|
||||
return Enc.bufToHex(hex)
|
||||
.match(/.{2}/g).join(' ')
|
||||
.match(/.{1,24}/g).join(' ')
|
||||
.match(/.{1,50}/g).join('\n');
|
||||
}).join('\n\n')
|
||||
document.querySelector('.js-jwk').innerText = JSON.stringify(ssh.jwk, null, 2);
|
||||
} catch(e) {
|
||||
ssh = { error: { message: e.message } };
|
||||
document.querySelector('.js-hex').innerText = '';
|
||||
document.querySelector('.js-jwk').innerText = JSON.stringify(ssh, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
$input.addEventListener('keyup', convert);
|
||||
convert();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
28
package.json
Normal file
28
package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "bluecrypt-ssh-to-jwk",
|
||||
"version": "1.0.0",
|
||||
"description": "SSH to JWK in < 150 lines of VanillaJS.",
|
||||
"homepage": "https://git.coolaj86.com/coolaj86/bluecrypt-ssh-to-jwk.js",
|
||||
"main": "ssh-to-jwk.js",
|
||||
"scripts": {
|
||||
"prepare": "uglifyjs ssh-to-jwk.js > ssh-to-jwk.min.js"
|
||||
},
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.coolaj86.com/coolaj86/bluecrypt-ssh-to-jwk.js"
|
||||
},
|
||||
"keywords": [
|
||||
"zero-dependency",
|
||||
"SSH-to-JWK",
|
||||
"RSA",
|
||||
"EC",
|
||||
"SSH",
|
||||
"JWK",
|
||||
"ECDSA"
|
||||
],
|
||||
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||
"license": "MPL-2.0"
|
||||
}
|
165
ssh-to-jwk.js
Normal file
165
ssh-to-jwk.js
Normal file
@ -0,0 +1,165 @@
|
||||
;(function (exports) {
|
||||
'use strict';
|
||||
|
||||
if (!exports.Enc) { exports.Enc = {}; }
|
||||
if (!exports.SSH) { exports.SSH = {}; }
|
||||
|
||||
var Enc = exports.Enc;
|
||||
var SSH = exports.SSH;
|
||||
|
||||
SSH.parse = function (ssh) {
|
||||
ssh = SSH.parseBlock(ssh);
|
||||
ssh = SSH.parseElements(ssh);
|
||||
//delete ssh.bytes;
|
||||
return SSH.parsePublicKey(ssh);
|
||||
};
|
||||
|
||||
SSH.parseBlock = function (ssh) {
|
||||
ssh = ssh.split(/\s+/g);
|
||||
|
||||
return {
|
||||
type: ssh[0]
|
||||
, bytes: Enc.base64ToBuf(ssh[1])
|
||||
, comment: ssh[2]
|
||||
};
|
||||
};
|
||||
|
||||
SSH.parseElements = function (ssh) {
|
||||
var buf = ssh.bytes;
|
||||
var fulllen = buf.byteLength || buf.length;
|
||||
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;
|
||||
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'));
|
||||
}
|
||||
|
||||
ssh.elements = els;
|
||||
return ssh;
|
||||
};
|
||||
|
||||
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) {
|
||||
ssh.jwk = {
|
||||
kty: 'RSA'
|
||||
, n: Enc.bufToUrlBase64(els[2])
|
||||
, e: Enc.bufToUrlBase64(els[1])
|
||||
};
|
||||
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);
|
||||
|
||||
// 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); }
|
||||
|
||||
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'
|
||||
};
|
||||
|
||||
Enc.base64ToBuf = function (b64) {
|
||||
return Enc.binToBuf(atob(b64));
|
||||
};
|
||||
|
||||
Enc.binToBuf = function (bin) {
|
||||
var arr = bin.split('').map(function (ch) {
|
||||
return ch.charCodeAt(0);
|
||||
});
|
||||
return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr;
|
||||
};
|
||||
|
||||
Enc.bufToBase64 = function (u8) {
|
||||
var bin = '';
|
||||
u8.forEach(function (i) {
|
||||
bin += String.fromCharCode(i);
|
||||
});
|
||||
return btoa(bin);
|
||||
};
|
||||
|
||||
Enc.bufToBin = function (buf) {
|
||||
var bin = '';
|
||||
// cannot use .map() because Uint8Array would return only 0s
|
||||
buf.forEach(function (ch) {
|
||||
bin += String.fromCharCode(ch);
|
||||
});
|
||||
return bin;
|
||||
};
|
||||
|
||||
Enc.bufToUrlBase64 = function (u8) {
|
||||
return Enc.bufToBase64(u8)
|
||||
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
};
|
||||
|
||||
Enc.urlBase64ToBase64 = function urlsafeBase64ToBase64(str) {
|
||||
var r = str % 4;
|
||||
if (2 === r) {
|
||||
str += '==';
|
||||
} else if (3 === r) {
|
||||
str += '=';
|
||||
}
|
||||
return str.replace(/-/g, '+').replace(/_/g, '/');
|
||||
};
|
||||
|
||||
}('undefined' !== typeof window ? window : module.exports));
|
1
ssh-to-jwk.min.js
vendored
Normal file
1
ssh-to-jwk.min.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
(function(exports){"use strict";if(!exports.Enc){exports.Enc={}}if(!exports.SSH){exports.SSH={}}var Enc=exports.Enc;var SSH=exports.SSH;SSH.parse=function(ssh){ssh=SSH.parseBlock(ssh);ssh=SSH.parseElements(ssh);return SSH.parsePublicKey(ssh)};SSH.parseBlock=function(ssh){ssh=ssh.split(/\s+/g);return{type:ssh[0],bytes:Enc.base64ToBuf(ssh[1]),comment:ssh[2]}};SSH.parseElements=function(ssh){var buf=ssh.bytes;var fulllen=buf.byteLength||buf.length;var offset=buf.byteOffset||0;var i=0;var index=0;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;el=buf.slice(index,index+len);if(0===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"))}ssh.elements=els;return ssh};SSH.parsePublicKey=function(ssh){var els=ssh.elements;var typ=Enc.bufToBin(els[0]);var len;if(SSH.types.rsa===typ){ssh.jwk={kty:"RSA",n:Enc.bufToUrlBase64(els[2]),e:Enc.bufToUrlBase64(els[1])};return ssh}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]))}var x=els[2].slice(1,1+len);var y=els[2].slice(1+len,1+len+len);if(0===x[0]){x=x.slice(1)}if(0===y[0]){y=y.slice(1)}ssh.jwk.x=Enc.bufToUrlBase64(x);ssh.jwk.y=Enc.bufToUrlBase64(y);return ssh};SSH.types={p256:"ecdsa-sha2-nistp256",p384:"ecdsa-sha2-nistp384",rsa:"ssh-rsa"};Enc.base64ToBuf=function(b64){return Enc.binToBuf(atob(b64))};Enc.binToBuf=function(bin){var arr=bin.split("").map(function(ch){return ch.charCodeAt(0)});return"undefined"!==typeof Uint8Array?new Uint8Array(arr):arr};Enc.bufToBase64=function(u8){var bin="";u8.forEach(function(i){bin+=String.fromCharCode(i)});return btoa(bin)};Enc.bufToBin=function(buf){var bin="";buf.forEach(function(ch){bin+=String.fromCharCode(ch)});return bin};Enc.bufToUrlBase64=function(u8){return Enc.bufToBase64(u8).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")};Enc.urlBase64ToBase64=function urlsafeBase64ToBase64(str){var r=str%4;if(2===r){str+="=="}else if(3===r){str+="="}return str.replace(/-/g,"+").replace(/_/g,"/")}})("undefined"!==typeof window?window:module.exports);
|
Loading…
x
Reference in New Issue
Block a user