commit 86ee62e0d31594518c8adaa3a5dd6ab2a6810b56 Author: AJ ONeal Date: Sat Dec 1 22:13:03 2018 -0700 v1.0.0: parse SSH public keys in the browser diff --git a/README.md b/README.md new file mode 100644 index 0000000..2bc1362 --- /dev/null +++ b/README.md @@ -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 + + + +```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 + +``` + +```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) diff --git a/index.html b/index.html new file mode 100644 index 0000000..fb5976a --- /dev/null +++ b/index.html @@ -0,0 +1,68 @@ + + + + SSH Pub Parser - Bluecrypt + + + +

Bluecrypt SSH Public Key Parser

+ + + +
 
+ +
 
+ +
+

Made with ssh-to-jwk.js

+ + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..21b9fbb --- /dev/null +++ b/package.json @@ -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 (https://coolaj86.com/)", + "license": "MPL-2.0" +} diff --git a/ssh-to-jwk.js b/ssh-to-jwk.js new file mode 100644 index 0000000..ece1cf0 --- /dev/null +++ b/ssh-to-jwk.js @@ -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)); diff --git a/ssh-to-jwk.min.js b/ssh-to-jwk.min.js new file mode 100644 index 0000000..5ee5a5e --- /dev/null +++ b/ssh-to-jwk.min.js @@ -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(index15){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);