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