Compare commits
	
		
			No commits in common. "master" and "v1.0.2" have entirely different histories.
		
	
	
		
	
		
							
								
								
									
										46
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								README.md
									
									
									
									
									
								
							| @ -5,23 +5,19 @@ and convert it into a public JWK. | |||||||
| 
 | 
 | ||||||
| Works for RSA and ECDSA public keys. | Works for RSA and ECDSA public keys. | ||||||
| 
 | 
 | ||||||
| # Features | Features | ||||||
|  | ======== | ||||||
| 
 | 
 | ||||||
| < 100 lines of code | <1kb gzipped | 1.8kb minified | 3.1kb with comments | < 100 lines of code | <1kb gzipped | 1.8kb minified | 3.1kb with comments | ||||||
| 
 | 
 | ||||||
| * [x] SSH Public Keys ([RFC 4253](https://coolaj86.com/articles/the-ssh-public-key-format/)) | * [x] SSH Public Keys | ||||||
|   * fingerprint | * [x] RSA Public Keys | ||||||
| * [x] OpenSSH Private Keys |  | ||||||
| * [x] RSA |  | ||||||
|   * 2048, 3072, 4096 |  | ||||||
| * [x] EC Public Keys | * [x] EC Public Keys | ||||||
|   * P-256 (prime256v1, secp256r1) |   * P-256 (prime256v1, secp256r1) | ||||||
|   * P-384 (secp384r1) |   * P-384 (secp384r1) | ||||||
| * [x] Browser Version | * [x] Browser Version | ||||||
|   * [Bluecrypt SSH to JWK](https://git.coolaj86.com/coolaj86/bluecrypt-ssh-to-jwk.js) |   * [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? | ### Need JWK to SSH? SSH to PEM? | ||||||
| 
 | 
 | ||||||
| Try one of these: | Try one of these: | ||||||
| @ -32,12 +28,9 @@ Try one of these: | |||||||
| 
 | 
 | ||||||
| ### Need SSH Private Keys? | ### Need SSH Private Keys? | ||||||
| 
 | 
 | ||||||
| Many SSH private keys are just normal PEM files, | SSH private keys are just normal PEM files, | ||||||
| so you can use Eckles or Rasha, as mentioned above. | 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 | # CLI | ||||||
| 
 | 
 | ||||||
| You can install `ssh-to-jwk` and use it from command line: | You can install `ssh-to-jwk` and use it from command line: | ||||||
| @ -50,43 +43,22 @@ npm install -g ssh-to-jwk | |||||||
| ssh-to-jwk ~/.ssh/id_rsa.pub | ssh-to-jwk ~/.ssh/id_rsa.pub | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ```bash |  | ||||||
| ssh-to-jwk ~/.ssh/id_rsa |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| # Usage | # Usage | ||||||
| 
 | 
 | ||||||
| You can also use it from JavaScript: | You can also use it from JavaScript: | ||||||
| 
 | 
 | ||||||
| **SSH to JWK** |  | ||||||
| 
 |  | ||||||
| ```js | ```js | ||||||
| var fs = require('fs'); | var fs = require('fs'); | ||||||
| var sshtojwk = require('ssh-to-jwk'); | var sshtojwk = require('ssh-to-jwk'); | ||||||
| var ssh; |  | ||||||
| 
 | 
 | ||||||
| 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); |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| **SSH Fingerprint** |  | ||||||
| 
 |  | ||||||
| ```js |  | ||||||
| var fs = require('fs'); |  | ||||||
| var sshtojwk = require('ssh-to-jwk'); |  | ||||||
| var pub = fs.readFileSync("./id_rsa.pub"); | var pub = fs.readFileSync("./id_rsa.pub"); | ||||||
|  | var ssh = sshtojwk.parse(pub); | ||||||
| 
 | 
 | ||||||
| sshtojwk.fingerprint({ pub: pub }).then(function (fingerprint) { | console.info(ssh.jwk); | ||||||
|   console.info(fingerprint); |  | ||||||
|   // SHA256:yCB62vBVsOwqksgYwy/WDbaMF2PhPijAwcrlzmrxfko |  | ||||||
| }); |  | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| # Legal | Legal | ||||||
|  | ----- | ||||||
| 
 | 
 | ||||||
| [ssh-to-jwk.js](https://git.coolaj86.com/coolaj86/ssh-to-jwk.js) | | [ssh-to-jwk.js](https://git.coolaj86.com/coolaj86/ssh-to-jwk.js) | | ||||||
| MPL-2.0 | | MPL-2.0 | | ||||||
|  | |||||||
| @ -6,27 +6,12 @@ var path = require('path'); | |||||||
| var sshtojwk = require('../index.js'); | var sshtojwk = require('../index.js'); | ||||||
| 
 | 
 | ||||||
| var pubfile = process.argv[2]; | var pubfile = process.argv[2]; | ||||||
| var pub = process.argv[3]; |  | ||||||
| 
 | 
 | ||||||
| if (!pubfile) { | if (!pubfile) { | ||||||
|   pubfile = path.join(require('os').homedir(), '.ssh/id_rsa.pub'); |   pubfile = path.join(require('os').homedir(), '.ssh/id_rsa.pub'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var buf = fs.readFileSync(pubfile); | var buf = fs.readFileSync(pubfile); | ||||||
| var txt = buf.toString('ascii'); | var ssh = sshtojwk.parse(buf.toString('ascii')); | ||||||
| var opts = { public: 'public' === pub }; |  | ||||||
| var ssh; |  | ||||||
| 
 | 
 | ||||||
| if ('-' === txt[0]) { | console.info(JSON.stringify(ssh.jwk, null, 2)); | ||||||
|   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)); |  | ||||||
| }); |  | ||||||
|  | |||||||
| @ -1,7 +0,0 @@ | |||||||
| { |  | ||||||
|   "kty": "EC", |  | ||||||
|   "crv": "P-256", |  | ||||||
|   "d": "iYydo27aNGO9DBUWeGEPD8oNi1LZDqfxPmQlieLBjVQ", |  | ||||||
|   "x": "IT1SWLxsacPiE5Z16jkopAn8_-85rMjgyCokrnjDft4", |  | ||||||
|   "y": "mP2JwOAOdMmXuwpxbKng3KZz27mz-nKWIlXJ3rzSGMo" |  | ||||||
| } |  | ||||||
| @ -1,9 +0,0 @@ | |||||||
| -----BEGIN OPENSSH PRIVATE KEY----- |  | ||||||
| b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS |  | ||||||
| 1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQQhPVJYvGxpw+ITlnXqOSikCfz/7zms |  | ||||||
| yODIKiSueMN+3pj9icDgDnTJl7sKcWyp4Nymc9u5s/pyliJVyd680hjKAAAAqGJjanNiY2 |  | ||||||
| pzAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCE9Uli8bGnD4hOW |  | ||||||
| deo5KKQJ/P/vOazI4MgqJK54w37emP2JwOAOdMmXuwpxbKng3KZz27mz+nKWIlXJ3rzSGM |  | ||||||
| oAAAAhAImMnaNu2jRjvQwVFnhhDw/KDYtS2Q6n8T5kJYniwY1UAAAADnJvb3RAbG9jYWxo |  | ||||||
| b3N0AQ== |  | ||||||
| -----END OPENSSH PRIVATE KEY----- |  | ||||||
| @ -1,7 +0,0 @@ | |||||||
| { |  | ||||||
|   "kty": "EC", |  | ||||||
|   "crv": "P-384", |  | ||||||
|   "d": "XlyuCEWSTTS8U79O_Mz05z18vh4kb10szvu_7pdXuGWV6lfEyPExyUYWsA6A2kdV", |  | ||||||
|   "x": "2zEU0bKCa7ejKLIJ8oPGnLhqhxyiv4_w38K2a0SPC6dsSd9_glNJ8lcqv0sff5Gb", |  | ||||||
|   "y": "VD4jnu83S6scn6_TeAj3EZOREGbOs6dzoVpaugn-XQMMyC9O4VLbDDFGBZTJlMsb" |  | ||||||
| } |  | ||||||
| @ -1,10 +0,0 @@ | |||||||
| -----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----- |  | ||||||
| @ -1,11 +0,0 @@ | |||||||
| { |  | ||||||
|   "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" |  | ||||||
| } |  | ||||||
| @ -1,27 +0,0 @@ | |||||||
| -----BEGIN OPENSSH PRIVATE KEY----- |  | ||||||
| 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 |  | ||||||
| -----END OPENSSH PRIVATE KEY----- |  | ||||||
| @ -1 +0,0 @@ | |||||||
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBH1Zk94FK+LGSGNA6H3IJdeUTHkI30zKH3gIknWuKB/zr3eBz3QeZqEhHmgwYfCWoyhLuQqtHD7gqnGqwXssa/E= aj@bowie.local |  | ||||||
| @ -1 +0,0 @@ | |||||||
| ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBMM1wvONZSobkf6CtT9n5E06y+ofhXmpTVSWRTOpMiZi+bsKViBi1eptF4LMufUT4M5DDU3V/kT9iM8124uWSWwS/ernmTsENNOI2TlzXkKkJshXh5Z4tFIVIkEZAeW/6w== aj@bowie.local |  | ||||||
| @ -1 +0,0 @@ | |||||||
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDTNGUXvKQQ2sbdXdILzeI7u60ziBDHqCbrYxUkdro0LA8SqimkQexHKd9SBVvrxNm3l1LsCRmeIgrxlhwuptsygK80rRX5Jit2DCRP62tyMBIEIqLxAksMEW1z6M3XIdIj2jWkL6ei6Qver03JWt4TfxZG94axSPxPaHYf6Psb+lZnMfYrWXtVD9hJA6vQjKNqmpUo1I2dWq6e6F3Oepejfu9Cz1yMnEsNJG3COubJSXUkEdsJYSrW3wfUnMPBrlnjs8uP4NBTik+5SHmpb+pU3+2DjgC4tsnTIkiusaLIp/qJmS+X18pFzRCMy2jfX1QeHMPsXnBSvdfyIdZfIVs5 aj@bowie.local |  | ||||||
| @ -10,20 +10,6 @@ Enc.base64ToBuf = function (str) { | |||||||
|   return Buffer.from(str, 'base64'); |   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) { | Enc.bufToBase64 = function (u8) { | ||||||
|   return Buffer.from(u8).toString('base64'); |   return Buffer.from(u8).toString('base64'); | ||||||
| }; | }; | ||||||
| @ -37,5 +23,6 @@ Enc.bufToHex = function (u8) { | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| Enc.bufToUrlBase64 = function (u8) { | Enc.bufToUrlBase64 = function (u8) { | ||||||
|   return Enc.base64ToUrlBase64(Enc.bufToBase64(u8)); |   return Enc.bufToBase64(u8) | ||||||
|  |     .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); | ||||||
| }; | }; | ||||||
|  | |||||||
							
								
								
									
										28
									
								
								lib/pem.js
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								lib/pem.js
									
									
									
									
									
								
							| @ -1,28 +0,0 @@ | |||||||
| '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 + '-----' |  | ||||||
|   ; |  | ||||||
| }; |  | ||||||
| @ -1,126 +1,57 @@ | |||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| /*global BigInt*/ |  | ||||||
| var SSH = module.exports; | var SSH = module.exports; | ||||||
| var Enc = require('./encoding.js'); | var Enc = require('./encoding.js'); | ||||||
| var PEM = require('./pem.js'); |  | ||||||
| var bnwarn = false; |  | ||||||
| 
 | 
 | ||||||
| SSH.parse = function (opts) { | SSH.parse = function (ssh) { | ||||||
|   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 { |  | ||||||
|     ssh.elements = SSH.parseElements(ssh.bytes); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   //delete ssh.bytes;
 |  | ||||||
|   return SSH.parsePublicKey(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); |   ssh = ssh.split(/\s+/g); | ||||||
| 
 | 
 | ||||||
|   return { |   var result = { type: ssh[0], jwk: null, comment: ssh[2] || '' }; | ||||||
|     type: ssh[0] |   var buf = Enc.base64ToBuf(ssh[1]); | ||||||
|   , bytes: Enc.base64ToBuf(ssh[1]) |   var els = SSH.parseElements({ bytes: buf }).elements; | ||||||
|   , comment: ssh[2] |   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' }; | ||||||
|  |   } 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
 | ||||||
|  |   while (0x00 === x[0]) { x = x.slice(1); } | ||||||
|  |   while (0x00 === y[0]) { y = y.slice(1); } | ||||||
|  | 
 | ||||||
|  |   result.jwk.x = Enc.bufToUrlBase64(x); | ||||||
|  |   result.jwk.y = Enc.bufToUrlBase64(y); | ||||||
|  | 
 | ||||||
|  |   return result; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| SSH.parsePrivateElements = function (ssh) { | SSH.parseElements = function (ssh) { | ||||||
|   // https://coolaj86.com/articles/the-openssh-private-key-format/
 |  | ||||||
|   var buf = ssh.bytes; |   var buf = ssh.bytes; | ||||||
|   var fulllen = buf.byteLength || buf.length; |   var fulllen = buf.byteLength || buf.length; | ||||||
|   var offset = (buf.byteOffset || 0); |   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 i = 0; | ||||||
|   var index = 0; |   var index = 0; | ||||||
|   // using dataview to be browser-compatible (I do want _some_ code reuse)
 |   // using dataview to be browser-compatible (I do want _some_ code reuse)
 | ||||||
| @ -134,7 +65,6 @@ SSH.parseElements = function (buf) { | |||||||
|     if (i > 15) { throw new Error("15+ elements, probably not a public ssh key"); } |     if (i > 15) { throw new Error("15+ elements, probably not a public ssh key"); } | ||||||
|     len = dv.getUint32(index, false); |     len = dv.getUint32(index, false); | ||||||
|     index += 4; |     index += 4; | ||||||
|     if (0 === len) { continue; } |  | ||||||
|     el = buf.slice(index, index + len); |     el = buf.slice(index, index + len); | ||||||
|     // remove BigUInt '00' prefix
 |     // remove BigUInt '00' prefix
 | ||||||
|     if (0x00 === el[0]) { |     if (0x00 === el[0]) { | ||||||
| @ -144,90 +74,12 @@ SSH.parseElements = function (buf) { | |||||||
|     index += len; |     index += len; | ||||||
|   } |   } | ||||||
|   if (fulllen !== index) { |   if (fulllen !== index) { | ||||||
|     throw new Error("invalid ssh public key length \n" + els.map(function (b) { |     throw new Error(els.map(function (b) { | ||||||
|       return Enc.bufToHex(b); |       return Enc.bufToHex(b); | ||||||
|     }).join('\n')); |     }).join('\n') + "invalid ssh public key length"); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return els; |   ssh.elements = 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; |   return ssh; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "ssh-to-jwk", |   "name": "ssh-to-jwk", | ||||||
|   "version": "1.2.6", |   "version": "1.0.2", | ||||||
|   "description": "💯 SSH to JWK in a lightweight, zero-dependency library.", |   "description": "💯 SSH to JWK in a lightweight, zero-dependency library.", | ||||||
|   "homepage": "https://git.coolaj86.com/coolaj86/ssh-to-jwk.js", |   "homepage": "https://git.coolaj86.com/coolaj86/ssh-to-jwk.js", | ||||||
|   "main": "index.js", |   "main": "index.js", | ||||||
| @ -29,8 +29,6 @@ | |||||||
|     "RSA", |     "RSA", | ||||||
|     "EC", |     "EC", | ||||||
|     "SSH", |     "SSH", | ||||||
|     "OpenSSH", |  | ||||||
|     "fingerprint", |  | ||||||
|     "JWK", |     "JWK", | ||||||
|     "ECDSA" |     "ECDSA" | ||||||
|   ], |   ], | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user