v1.0.0: generate ssh-keygen compatible fingerprints
This commit is contained in:
commit
08e9349ee7
81
README.md
Normal file
81
README.md
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# Bluecrypt SSH Fingerprint (for Browsers)
|
||||||
|
|
||||||
|
A minimal library to read an SSH public key (`id_rsa.pub`)
|
||||||
|
and generate its fingerprint.
|
||||||
|
|
||||||
|
Works for RSA and ECDSA public keys.
|
||||||
|
|
||||||
|
# Features
|
||||||
|
|
||||||
|
< 150 lines of code | 1.1kb gzipped | 2.4kb minified | 4.1kb with comments
|
||||||
|
|
||||||
|
* [x] SSH Public Key SHA256 Fingerprints
|
||||||
|
* RSA (2048-bit, 3072-bit, 4096-bit)
|
||||||
|
* EC / ECDSA
|
||||||
|
* P-256 (prime256v1, secp256r1)
|
||||||
|
* P-384 (secp384r1)
|
||||||
|
* [x] Node.js version
|
||||||
|
* [Greenlock SSH Fingerprint](https://git.coolaj86.com/coolaj86/greenlock-ssh-fingerprint.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-fingerprint/>
|
||||||
|
|
||||||
|
<img border="1" src="https://git.coolaj86.com/coolaj86/ssh-fingerprint.js/raw/branch/master/screenshot.png" />
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.coolaj86.com/coolaj86/bluecrypt-ssh-fingerprint.js
|
||||||
|
pushd bluecrypt-ssh-fingerprint.js/
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
open index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
# Install
|
||||||
|
|
||||||
|
You can use it as a plain-old javascript library:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="https://git.coolaj86.com/coolaj86/bluecrypt-ssh-fingerprint.js/raw/branch/master/ssh-fingerprint.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
It's also on npm:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install bluecrypt-ssh-fingerprint
|
||||||
|
```
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
Very simple:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var pub = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCba21UHE+VbDTpmYYFZUOV+OQ8AngOCdjROsPC0KiEfMvEaEM3NQl58u6QL7G7QsErKViiNPm9OTFo6HF5JijfWzK7haHFuRMEsgI4VwIYyhvqlJDfw/wt0AiVvSmoMfEQn1p1aiaO4V/RJSE3Vw/uz2bxiT22uSkSqOyShyfYE6dMHnuoBkzr4jvSifT+INmbv6Nyo4+AAMCZtYeHLrsFeSTjLL9jMPjI4ZkVdlw2n3Xn9NbltF3/8Ao8dQfElqw+LIQWqU0oFHYNIP4ttfl5ObMKHaKSvBMyNruZR0El/ZsrcHLkAHRCLj07KRQJ81l5CUTPtQ02P1Eamz/nT4I3 rsa@localhost';
|
||||||
|
|
||||||
|
SSH.fingerprint({ pub: pub }).then(function (fing) {
|
||||||
|
console.info('The key fingerprint is:');
|
||||||
|
console.info(fing.fingerprint);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
# Other Tools in the Bluecrypt Suite
|
||||||
|
|
||||||
|
* [Browser JWK to SSH](https://git.coolaj86.com/coolaj86/bluecrypt-jwk-to-ssh.js)
|
||||||
|
* [Browser SSH to JWK](https://git.coolaj86.com/coolaj86/bluecrypt-ssh-to-jwk.js)
|
||||||
|
* [Browser ASN.1 parser](https://git.coolaj86.com/coolaj86/asn1-parser.js) (for x509, etc)
|
||||||
|
* [Browser ASN.1 packer](https://git.coolaj86.com/coolaj86/asn1-packer.js) (for x509, etc)
|
||||||
|
|
||||||
|
# Legal
|
||||||
|
|
||||||
|
[ssh-fingerprint.js](https://git.coolaj86.com/coolaj86/bluecrypt-ssh-fingerprint.js) |
|
||||||
|
MPL-2.0 |
|
||||||
|
[Terms of Use](https://therootcompany.com/legal/#terms) |
|
||||||
|
[Privacy Policy](https://therootcompany.com/legal/#privacy)
|
||||||
|
|
||||||
|
Bluecrypt™ is owned by AJ ONeal
|
66
index.html
Normal file
66
index.html
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>SSH Fingerprint Generator - Bluecrypt</title>
|
||||||
|
<style>
|
||||||
|
textarea {
|
||||||
|
width: 42em;
|
||||||
|
height: 10em;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.code {
|
||||||
|
width: 31em;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Bluecrypt SSH Fingerprint Generator</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-fingerprint"> </code></pre>
|
||||||
|
|
||||||
|
<div class="code">
|
||||||
|
<pre><code class="js-json"> </code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<p>Made with <a href="https://git.coolaj86.com/coolaj86/bluecrypt-ssh-fingerprint.js/">ssh-fingerprint.js</a></p>
|
||||||
|
|
||||||
|
<script src="./ssh-fingerprint.js"></script>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var $input = document.querySelector('.js-input');
|
||||||
|
|
||||||
|
function convert() {
|
||||||
|
console.log('keyup');
|
||||||
|
|
||||||
|
try {
|
||||||
|
var pub = document.querySelector('.js-input').value.trim();
|
||||||
|
SSH.fingerprint({ pub: pub }).then(function (fing) {
|
||||||
|
var arr = [];
|
||||||
|
console.log(fing);
|
||||||
|
document.querySelector('.js-fingerprint').innerText = 'The key fingerprint is:\n'
|
||||||
|
+ fing.fingerprint + ' ' + fing.comment;
|
||||||
|
fing.digest.forEach(function (i) { arr.push(i); });
|
||||||
|
fing.digest = 'Uint8Array <' + arr.join() + '>';
|
||||||
|
document.querySelector('.js-json').innerText = JSON.stringify(fing, null, 1);
|
||||||
|
}).catch(function (e) {
|
||||||
|
var msg = { error: { message: e.message } };
|
||||||
|
document.querySelector('.js-fingerprint').innerText = JSON.stringify(msg, null, 2);
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
var msg = { error: { message: e.message } };
|
||||||
|
document.querySelector('.js-fingerprint').innerText = JSON.stringify(msg, null, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$input.addEventListener('keyup', convert);
|
||||||
|
convert();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
27
package.json
Normal file
27
package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "bluecrypt-ssh-fingerprint",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "SSH Fingerprint in < 150 lines of VanillaJS, part of the Bluecrypt suite for Browser Crypto.",
|
||||||
|
"homepage": "https://git.coolaj86.com/coolaj86/bluecrypt-ssh-fingerprint.js",
|
||||||
|
"main": "ssh-fingerprint.js",
|
||||||
|
"scripts": {
|
||||||
|
"prepare": "uglifyjs ssh-fingerprint.js > ssh-fingerprint.min.js"
|
||||||
|
},
|
||||||
|
"directories": {
|
||||||
|
"lib": "lib"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.coolaj86.com/coolaj86/bluecrypt-ssh-fingerprint.js"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"zero-dependency",
|
||||||
|
"SSH",
|
||||||
|
"Fingerprint",
|
||||||
|
"rsa",
|
||||||
|
"ec",
|
||||||
|
"ecdsa"
|
||||||
|
],
|
||||||
|
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||||
|
"license": "MPL-2.0"
|
||||||
|
}
|
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 92 KiB |
157
ssh-fingerprint.js
Normal file
157
ssh-fingerprint.js
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
;(function (exports) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
if (!exports.Enc) { exports.Enc = {}; }
|
||||||
|
if (!exports.SSH) { exports.SSH = {}; }
|
||||||
|
|
||||||
|
var Enc = exports.Enc;
|
||||||
|
var SSH = exports.SSH;
|
||||||
|
|
||||||
|
SSH.fingerprint = function (opts) {
|
||||||
|
var ssh;
|
||||||
|
if (opts.bytes) {
|
||||||
|
ssh = opts;
|
||||||
|
} else {
|
||||||
|
ssh = SSH.parseBlock(opts.pub);
|
||||||
|
}
|
||||||
|
ssh = SSH.parseElements(ssh);
|
||||||
|
|
||||||
|
// for browser compat
|
||||||
|
return window.crypto.subtle.digest('SHA-256', ssh.bytes).then(function (digest) {
|
||||||
|
digest = new Uint8Array(digest);
|
||||||
|
// 2048 SHA256:yCB62vBVsOwqksgYwy/WDbaMF2PhPijAwcrlzmrxfko rsa@localhost (RSA)
|
||||||
|
// 256 SHA256:wcH95nkL7ZkeRURHpLuoThIBEIIKkYgf9etD18PIx40 P-256@localhost (ECDSA)
|
||||||
|
ssh = SSH.parseKeyType(ssh);
|
||||||
|
return {
|
||||||
|
type: 'sha256'
|
||||||
|
, digest: digest
|
||||||
|
, fingerprint: 'SHA256:' + Enc.bufToBase64(digest).replace(/=+$/g, '')
|
||||||
|
, size: ssh.size
|
||||||
|
, comment: ssh.comment
|
||||||
|
, kty: ssh.kty
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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.parseKeyType = function (ssh) {
|
||||||
|
var els = ssh.elements;
|
||||||
|
var typ = Enc.bufToBin(els[0]);
|
||||||
|
|
||||||
|
// RSA keys are all the same
|
||||||
|
if (SSH.types.rsa === typ) {
|
||||||
|
ssh.kty = 'RSA';
|
||||||
|
ssh.size = (ssh.elements[2].byteLength || ssh.lements[2].length);
|
||||||
|
return ssh;
|
||||||
|
}
|
||||||
|
|
||||||
|
// EC keys are each different
|
||||||
|
if (SSH.types.p256 === typ) {
|
||||||
|
ssh.kty = 'EC';
|
||||||
|
ssh.size = 32;
|
||||||
|
} else if (SSH.types.p384 === typ) {
|
||||||
|
ssh.kty = 'EC';
|
||||||
|
ssh.size = 48;
|
||||||
|
} else {
|
||||||
|
throw new Error("Unsupported ssh public key type: "
|
||||||
|
+ Enc.bufToBin(els[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
}('undefined' !== typeof window ? window : module.exports));
|
Loading…
x
Reference in New Issue
Block a user