Skip to content

Instantly share code, notes, and snippets.

@matvore
Last active April 16, 2020 14:36
Show Gist options
  • Save matvore/220f7e61f2529c2d6e3033c3eb5fc16d to your computer and use it in GitHub Desktop.
Save matvore/220f7e61f2529c2d6e3033c3eb5fc16d to your computer and use it in GitHub Desktop.
<!doctype html>
<html>
<head>
<title>js-keygen</title>
<script>
// adapted from https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-08#appendix-C
function base64urlEncode(arg) {
const step1 = window.btoa(arg); // Regular base64 encoder
const step2 = step1.split("=")[0]; // Remove any trailing '='s
const step3 = step2.replace(/\+/g, "-"); // 62nd char of encoding
const step4 = step3.replace(/\//g, "_"); // 63rd char of encoding
return step4;
}
function base64urlDecode(s) {
const step1 = s.replace(/-/g, "+"); // 62nd char of encoding
const step2 = step1.replace(/_/g, "/"); // 63rd char of encoding
let step3 = step2;
switch (step2.length % 4) { // Pad with trailing '='s
case 0: // No pad chars in this case
break;
case 2: // Two pad chars
step3 += "==";
break;
case 3: // One pad char
step3 += "=";
break;
default:
throw new Error("Illegal base64url string!");
}
return window.atob(step3); // Regular base64 decoder
}
function arrayToString(a) {
return String.fromCharCode.apply(null, a);
}
function stringToArray(s) {
return s.split("").map(c => c.charCodeAt());
}
function base64urlToArray(s) {
return stringToArray(base64urlDecode(s));
}
function pemToArray(pem) {
return stringToArray(window.atob(pem));
}
function arrayToPem(a) {
return window.btoa(a.map(c => String.fromCharCode(c)).join(""));
}
function arrayToLen(a) {
let result = 0;
for (let i = 0; i < a.length; i += 1) {
result = result * 256 + a[i];
}
return result;
}
function integerToOctet(n) {
const result = [];
for (let i = n; i > 0; i >>= 8) {
result.push(i & 0xff);
}
return result.reverse();
}
function lenToArray(n) {
const oct = integerToOctet(n);
let i;
for (i = oct.length; i < 4; i += 1) {
oct.unshift(0);
}
return oct;
}
function decodePublicKey(s) {
const split = s.split(" ");
const prefix = split[0];
if (prefix !== "ssh-rsa") {
throw new Error(`Unknown prefix: ${prefix}`);
}
const buffer = pemToArray(split[1]);
const nameLen = arrayToLen(buffer.splice(0, 4));
const type = arrayToString(buffer.splice(0, nameLen));
if (type !== "ssh-rsa") {
throw new Error(`Unknown key type: ${type}`);
}
const exponentLen = arrayToLen(buffer.splice(0, 4));
const exponent = buffer.splice(0, exponentLen);
const keyLen = arrayToLen(buffer.splice(0, 4));
const key = buffer.splice(0, keyLen);
return { type, exponent, key, name: split[2] };
}
function checkHighestBit(v) {
if (v[0] >> 7 === 1) {
// add leading zero if first bit is set
v.unshift(0);
}
return v;
}
function jwkToInternal(jwk) {
return {
type: "ssh-rsa",
exponent: checkHighestBit(stringToArray(base64urlDecode(jwk.e))),
name: "name",
key: checkHighestBit(stringToArray(base64urlDecode(jwk.n))),
};
}
function encodePublicKey(jwk, name) {
const k = jwkToInternal(jwk);
k.name = name;
const keyLenA = lenToArray(k.key.length);
const exponentLenA = lenToArray(k.exponent.length);
const typeLenA = lenToArray(k.type.length);
const array = [].concat(typeLenA, stringToArray(k.type), exponentLenA, k.exponent, keyLenA, k.key);
const encoding = arrayToPem(array);
return `${k.type} ${encoding} ${k.name}`;
}
function asnEncodeLen(n) {
let result = [];
if (n >> 7) {
result = integerToOctet(n);
result.unshift(0x80 + result.length);
} else {
result.push(n);
}
return result;
}
function encodePrivateKey(jwk) {
const order = ["n", "e", "d", "p", "q", "dp", "dq", "qi"];
const list = order.map(prop => {
const v = checkHighestBit(stringToArray(base64urlDecode(jwk[prop])));
const len = asnEncodeLen(v.length);
return [0x02].concat(len, v); // int tag is 0x02
});
let seq = [0x02, 0x01, 0x00]; // extra seq for SSH
seq = seq.concat(...list);
const len = asnEncodeLen(seq.length);
const a = [0x30].concat(len, seq); // seq is 0x30
return arrayToPem(a);
}
var generateKeyPair;
function copy(id) {
return function() {
var ta = document.querySelector(id);
ta.focus();
ta.select();
try {
var successful = document.execCommand("copy");
var msg = successful ? "successful" : "unsuccessful";
console.log("Copy key command was " + msg);
} catch (err) {
console.log("Oops, unable to copy");
}
window.getSelection().removeAllRanges();
ta.blur();
};
}
function buildHref(data) {
return "data:application/octet-stream;charset=utf-8;base64," + window.btoa(data);
}
document.addEventListener("DOMContentLoaded", function() {
document.querySelector("#savePrivate").addEventListener("click", function() {
document.querySelector("a#private").click();
});
document.querySelector("#copyPrivate").addEventListener("click", copy("#privateKey"));
document.querySelector("#savePublic").addEventListener("click", function() {
document.querySelector("a#public").click();
});
document.querySelector("#copyPublic").addEventListener("click", copy("#publicKey"));
document.querySelector("#generate").addEventListener("click", function() {
var name = document.querySelector("#name").value || "name";
document.querySelector("a#private").setAttribute("download", name + "_rsa");
document.querySelector("a#public").setAttribute("download", name + "_rsa.pub");
var alg = document.querySelector("#alg").value || "RSASSA-PKCS1-v1_5";
var size = parseInt(document.querySelector("#size").value || "2048", 10);
var hash = document.querySelector('#hash').value || 'SHA-1';
generateKeyPair(alg, size, hash, name)
.then(function(keys) {
document.querySelector("#private").setAttribute("href", buildHref(keys[0]));
document.querySelector("#public").setAttribute("href", buildHref(keys[1]));
document.querySelector("#privateKey").textContent = keys[0];
document.querySelector("#publicKey").textContent = keys[1];
document.querySelector("#result").style.display = "block";
})
.catch(function(err) {
console.error(err);
});
});
});
const extractable = true;
function wrap(text, len) {
const length = len || 72;
let result = "";
for (let i = 0; i < text.length; i += length) {
result += text.slice(i, i + length);
result += "\n";
}
return result;
}
function rsaPrivateKey(key) {
return `-----BEGIN RSA PRIVATE KEY-----\n${key}-----END RSA PRIVATE KEY-----`;
}
function arrayBufferToBase64(buffer) {
let binary = "";
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i += 1) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
function generateKeyPair(alg, size, hash, name) {
return window.crypto.subtle
.generateKey(
{
name: alg,
modulusLength: size, // can be 1024, 2048, or 4096
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: { name: hash }, // can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
},
extractable,
["sign", "verify"]
)
.then(key => {
const privateKey = window.crypto.subtle
.exportKey("jwk", key.privateKey)
.then(encodePrivateKey)
.then(wrap)
.then(rsaPrivateKey);
const publicKey = window.crypto.subtle.exportKey("jwk", key.publicKey).then(jwk => encodePublicKey(jwk, name));
return Promise.all([privateKey, publicKey]);
});
}
</script>
<style>
.donations_area {
font-size: 0.7em;
}
.address {
font-family: monospace;
}
body {
background-color: #cccccc;
font: 16px Arial, Tahoma, Helvetica, FreeSans, sans-serif;
}
button#generate {
background-color: rgb(60, 200, 30);
padding: 10px;
margin-top: 5px;
color: white;
font-weight: bold;
font-size: 1.1em;
border-radius: 8px;
border-width: 0px;
margin-right: 20px;
margin-left: 20px;
min-width: 120px;
}
div#content {
background-color: #f0f0f0;
border-radius: 8px;
width: 780px;
margin: auto;
padding: 20px;
}
label {
display: inline-block;
width: 80px;
}
select {
width: 200px;
}
input {
width: 180px;
}
textarea {
margin-top: 10px;
width: 650px;
height: 70px;
word-wrap: break-word;
font-family: monospace;
overflow: scroll;
}
</style>
</head>
<body>
<div id="content">
<h1>js-keygen</h1>
Generate a keypair to be used with openSSH, this replicate ssh-keygen function in javascript in the browser, using the webcrypto
api and a bit of glue.
<br> For an in-depth explanation on how this work, see the
<a href="http://blog.roumanoff.com/2015/09/using-webcrypto-api-to-generate-keypair.html">blog post</a>.
<br> Usually you would want to save the private key to the machine initiating the ssh connection, and you want to copy the
public key to the system receiving the connection.
<br> No data is being sent to the server, everything happens within the context of this web page.
<hr>
<div>
<label for="name">Name:</label>
<input id="name" type="text" value="webcrypto">
</div>
<div>
<label for="alg">Algorithm:</label>
<select id="alg">
<option value="RSASSA-PKCS1-v1_5" selected>RSASSA-PKCS1-v1_5</option>
<option value="RSA-PSS">RSA-PSS</option>
</select>
<label for="size">Size:</label>
<select id="size">
<option value="1024">1024</option>
<option value="2048" selected>2048</option>
<option value="4096">4096</option>
</select>
</div>
<div>
<label for="hash">Hash:</label>
<select id="hash">
<option value="SHA-1" selected>SHA-1</option>
<option value="SHA-256">SHA-256</option>
<option value="SHA-384">SHA-384</option>
<option value="SHA-512">SHA-512</option>
</select>
</div>
<label for="generate"></label>
<button id="generate">Generate</button>
<br>
<div id="result" style="display:none;">
<hr>
<a id="private" style="display: none;" href="" download="id_rsa">id_rsa</a>
<a id="public" style="display: none;" href="" download="id_rsa.pub">id_rsa.pub</a>
Private Key
<button id="copyPrivate">Copy</button> or
<button id="savePrivate">Save</button>
<br>
<textarea id="privateKey" style="height: 150px;" spellcheck="false"></textarea>
<hr> Public Key
<button id="copyPublic">Copy</button> or
<button id="savePublic">Save</button>
<br>
<textarea id="publicKey" spellcheck="false"></textarea>
</div>
<hr> Original version coded by <a href="http://blog.roumanoff.com">Patrick Roumanoff</a>
<br> Repackaged and fixed by <a href="mailto:[email protected]">matvore</a> for portability and easy downloading.
<div class="donations_area">
<br> Donations for matvore are welcome:
<br> <strong>BTC</strong> <span class="address">36KNnXm2or281Fwyc1eTmyHZusLS6Ga1DN</span>
<br> <strong>ETH</strong> <span class="address">0x8d77026817F4ffbd3d57c221920EDdcA48ae8c67</span>
</div>
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment