working state

This commit is contained in:
Patrick Roumanoff 2015-09-03 21:58:00 +10:00
commit aa49d05c58
9 changed files with 460 additions and 0 deletions

1
README.md Normal file
View file

@ -0,0 +1 @@
Generate a keypair using the webcrypto API

21
base64url.js Normal file
View file

@ -0,0 +1,21 @@
//adapted from https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-08#appendix-C
function base64urlEncode(s) {
var s = window.btoa(arg); // Regular base64 encoder
s = s.split('=')[0]; // Remove any trailing '='s
s = s.replace(/+/g, '-'); // 62nd char of encoding
s = s.replace(/\//g, '_'); // 63rd char of encoding
return s;
}
function base64urlDecode(s) {
s = s.replace(/-/g, '+'); // 62nd char of encoding
s = s.replace(/_/g, '/'); // 63rd char of encoding
switch (s.length % 4) { // Pad with trailing '='s
case 0: break; // No pad chars in this case
case 2: s += "=="; break; // Two pad chars
case 3: s += "="; break; // One pad char
default: throw "Illegal base64url string!";
}
return window.atob(s); // Standard base64 decoder
}

25
bower.json Normal file
View file

@ -0,0 +1,25 @@
{
"name": "js-keypair",
"version": "0.0.0",
"authors": [
"Patrick Roumanoff <patrick@roumanoff.com>"
],
"description": "generate keypair in the browser",
"main": "index.html",
"moduleType": [
"globals"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
},
"devDependencies": {
"qunit": "~1.19.0"
}
}

72
index.html Normal file
View file

@ -0,0 +1,72 @@
<!doctype html>
<html>
<head>
<title>Key Pair generation for SSH access</title>
<script src="base64url.js"></script>
<script src="ssh-util.js"></script>
<script src="keypair.js"></script>
<style>
label {
display: inline-block;
width: 80px;
}
select{
width: 200px;
}
input {
width: 180px;
}
</style>
</head>
<body>
<div><label for="name">Name:</label><input id="name" type="text" value="me@domain"></div>
<div><label for="alg">Algorithm:</label><select id="alg"><option value="RSASSA-PKCS1-v1_5" selected>RSASSA-PKCS1-v1_5</option></select>
<label for="size">Size:</label><input id="size" type="text" value="2048"></div>
<div><label for="hash">Hash:</label><select id="hash">
<option value="SHA-1">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>
<button id="generate">Generate</button>
<br>
<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="overflow: scroll; width: 650px; height: 150px; word-wrap:break-word;font-family: monospace;" spellcheck="false"></textarea>
<hr>
Public Key <button id="copyPublic">Copy</button> or <button id="savePublic">Save</button><br>
<textarea id="publicKey" style="width: 650px; height: 70px; word-wrap:break-word;font-family: monospace;" spellcheck="false"></textarea>
<script>
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();
}
}
document.querySelector('#savePrivate').addEventListener('click', function(event) {
document.querySelector('a#private').click();
});
document.querySelector('#copyPrivate').addEventListener('click', copy('#privateKey'));
document.querySelector('#savePublic').addEventListener('click', function(event) {
document.querySelector('a#public').click();
});
document.querySelector('#copyPublic').addEventListener('click', copy('#publicKey'));
</script>
</body>
</html>

70
keypair.js Normal file
View file

@ -0,0 +1,70 @@
var extractable = true;
function wrap(text, len) {
var length = len || 72, i, result = "";
for(i=0; i < text.length; i += length) {
result += text.slice(i, i + length) + "\n";
}
return result;
}
function rsaPrivateKey(key) {
return "-----BEGIN RSA PRIVATE KEY-----\n" + key + "-----END RSA PRIVATE KEY-----";
}
function arrayBufferToBase64( buffer ) {
var binary = '';
var bytes = new Uint8Array( buffer );
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode( bytes[ i ] );
}
return window.btoa( binary );
}
function generateKeyPair(alg, size, name) {
return window.crypto.subtle.generateKey({
name: "RSASSA-PKCS1-v1_5",
modulusLength: 2048, //can be 1024, 2048, or 4096
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: {name: "SHA-1"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
},
extractable,
["sign", "verify"]
).then(function(key){
var private = window.crypto.subtle.exportKey(
"jwk",
key.privateKey
).then(encodePrivateKey).then(wrap).then(rsaPrivateKey);
var public = window.crypto.subtle.exportKey(
"jwk",
key.publicKey
).then(function(jwk){
return encodePublicKey(jwk, name);
});
return Promise.all([private, public]);
});
}
function buildHref(data) {
return "data:application/octet-stream;charset=utf-8;base64," + window.btoa(data);
}
document.addEventListener("DOMContentLoaded", function(event) {
document.querySelector('#generate').addEventListener('click', function(event) {
var name = document.querySelector('#name').value || "name";
var alg = document.querySelector('#alg').value || "RSASSA-PKCS1-v1_5";
var size = parseInt(document.querySelector('#size').value || "2048");
generateKeyPair(alg, size, 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];
}).catch(function(err){
console.error(err);
});
});
});

14
package.json Normal file
View file

@ -0,0 +1,14 @@
{
"name": "js-keypair",
"version": "1.0.0",
"description": "Generate a key pair using the webcrypto API",
"main": "keypair.js",
"dependencies": {
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "patrick@roumanoff.com",
"license": "Apache 2.0"
}

17
qunit.html Normal file
View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>QUnit Example</title>
<link rel="stylesheet" href="bower_components/qunit/qunit/qunit.css">
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"><div id="generate"></div></div>
<script src="bower_components/qunit/qunit/qunit.js"></script>
<script src="base64url.js"></script>
<script src="ssh-util.js"></script>
<script src="keypair.js"></script>
<script src="tests.js"></script>
</body>
</html>

118
ssh-util.js Normal file
View file

@ -0,0 +1,118 @@
function arrayToString(a) {
return String.fromCharCode.apply(null, a);
}
function stringToArray(s) {
return s.split('').map(function (c) {
return 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(function (c) {
return String.fromCharCode(c);
}).join(''));
}
function arrayToLen(a) {
var result = 0, i;
for(i = 0; i < a.length; i++) {
result = result * 256 + a[i];
}
return result;
}
function integerToOctet(n) {
var result = [];
for(;n > 0; n = n >> 8 ) {
result.push(n & 0xFF);
}
return result.reverse();
}
function lenToArray(n) {
var oct = integerToOctet(n), i;
for(i = oct.length; i < 4; i++) {
oct.unshift(0);
}
return oct;
}
function decodePublicKey(s) {
var split = s.split(" ");
var prefix = split[0];
if(prefix != "ssh-rsa") {
throw ("Unknown prefix:" + prefix);
}
var buffer = pemToArray(split[1]);
var nameLen = arrayToLen(buffer.splice(0, 4));
var type = arrayToString(buffer.splice(0, nameLen));
if(type != "ssh-rsa") {
throw ("Unknown key type:" + type);
}
var exponentLen = arrayToLen(buffer.splice(0, 4));
var exponent = buffer.splice(0, exponentLen);
var keyLen = arrayToLen(buffer.splice(0, 4));
var key = buffer.splice(0, keyLen);
return {type: type, exponent: exponent, key: key, name: split[2]};
}
function jwkToInternal(jwk) {
return {
type: "ssh-rsa",
exponent: checkHighestBit(stringToArray(base64urlDecode(jwk.e))),
name: "name",
key: checkHighestBit(stringToArray(base64urlDecode(jwk.n)))
};
}
function checkHighestBit(v) {
if(v[0] >> 7 === 1) { // add leading zero if first bit is set
v.unshift(0);
}
return v;
}
function encodePublicKey(jwk, name) {
var k = jwkToInternal(jwk);
k.name = name;
var keyLenA = lenToArray(k.key.length);
var exponentLenA = lenToArray(k.exponent.length);
var typeLenA = lenToArray(k.type.length);
var array = [].concat(typeLenA, stringToArray(k.type), exponentLenA, k.exponent, keyLenA, k.key);
var encoding = arrayToPem(array);
return k.type + " " + encoding + " " + k.name;
}
function asnEncodeLen(n) {
var result = [];
if(n >> 7) {
result = integerToOctet(n);
result.unshift(0x80 + result.length);
} else {
result.push(n);
}
return result;
}
function encodePrivateKey(jwk) {
var order = ["n", "e", "d", "p", "q", "dp", "dq", "qi"];
var list = order.map(function (prop) {
var v = checkHighestBit(stringToArray(base64urlDecode(jwk[prop])));
var len = asnEncodeLen(v.length);
return [0x02].concat(len, v); // int tag is 0x02
});
var seq = [0x02, 0x01, 0x00]; // extra seq for SSH
seq = seq.concat.apply(seq, list);
var len = asnEncodeLen(seq.length);
var a = [0x30].concat(len , seq); // seq is 0x30
return arrayToPem(a);
}

122
tests.js Normal file
View file

@ -0,0 +1,122 @@
var public_ssh = ["ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCwi36YMW0eDS3NXSAM/Gcs0txeLOcZE0LQmGPYmHX09Fm1FC9AdzvDWQIfwVylqNy8G6X8+pE0TMuWav4rQjtWRls3j43LdrXkfaTZV2PNJH0ki2zaCND3cz46hBR1bSwi3O4LoN0ZHXoC4ZXoMBXKtYEOg+9jS+pE3vu2QSPruiRROTOYYvrjWx0Bwi8DJc90TmNVeqvPjewPAm4qaTdmh96jIgJQq+vAdhDHu90i31Kl3JUF94x6pzFmg8ZyXOv0Py2GtK9c5To3C33FXI8yTm/sf2Bp7fwd3MEGNcdVNqa7Tt0z2u5Jcmsws93SZuj4iVjbR6xqme9EmIa3BTB7 name",
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCuew50MaphQgiuM6H7zxMspNojI2Ujf77MuWlAjmw1JxcTkfE7JKzV+9fqmESJNtnZSr3+I2dxQhJ72jttrz+2dFt9ol91muTPWzKrA8XXIBH2o7sEJ+QB8/q7S03d+Zgw6tlo+qdXLOWcKqL5MJhYwzTFEdGTSMF00cBFadcpDq1xFPygGTHRa7m3pK723nGz7TMGWmtBK2bHx+Zlp7geLK/7hl+NRG1lTyIbtdkP2T4Y81Z0bhz9kNHroUei3MFD6HvN93qMJWl3/LZZzTb++1BedNeybGKqbtsB3xp0v3c6bQy49wR3RwrAwL03AKbCwTawAufSeoXyRI+rtgZ/ name",
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCpNNtjZeldPuJ5ZgcjO4i6eSJb6kiuf1sULOoWaW9acwSxAfqrmN6Hn7VGg3GK3kSKJUmBKMsF2u+ECajVBec+OTMlbL7oZrYNl2neUYsI7O0G/8lpozZjADYu8CaMqVSAeTa3ORga9Ht/qgCpqXIyEcTsFSbZ45hhaZF0fXQ0GHDCkV/ylBduQHxheCe1SPBSWIO2BwqSlGx/Q76lkL/BnGdcx7xVi3h2yNbEGxqzFuPK75VADZfWria4x09rTqvu41GWIyqzFcbB7BxNImVNh6WVk/qKTcXbfWwH8ck9Cd5bX9g36QaImZ6tW8i/bl3o75bGgP2hSWpsNx8CMVn9 name"];
var public_ssh_decoded = [{
type: "ssh-rsa",
exponent: [1,0,1],
name: "name",
key: [0,176,139,126,152,49,109,30,13,45,205,93,32,12,252,103,44,210,220,94,44,231,25,19,66,208,152,99,216,152,117,244,244,89,181,20,47,64,119,59,195,89,2,31,193,92,165,168,220,188,27,165,252,250,145,52,76,203,150,106,254,43,66,59,86,70,91,55,143,141,203,118,181,228,125,164,217,87,99,205,36,125,36,139,108,218,8,208,247,115,62,58,132,20,117,109,44,34,220,238,11,160,221,25,29,122,2,225,149,232,48,21,202,181,129,14,131,239,99,75,234,68,222,251,182,65,35,235,186,36,81,57,51,152,98,250,227,91,29,1,194,47,3,37,207,116,78,99,85,122,171,207,141,236,15,2,110,42,105,55,102,135,222,163,34,2,80,171,235,192,118,16,199,187,221,34,223,82,165,220,149,5,247,140,122,167,49,102,131,198,114,92,235,244,63,45,134,180,175,92,229,58,55,11,125,197,92,143,50,78,111,236,127,96,105,237,252,29,220,193,6,53,199,85,54,166,187,78,221,51,218,238,73,114,107,48,179,221,210,102,232,248,137,88,219,71,172,106,153,239,68,152,134,183,5,48,123]
}];
var jwk_public = {"alg":"RS1","e":"AQAB","ext":true,"key_ops":["verify"],"kty":"RSA","n":"3PWJ6uDsFPgQo67of3IYw0Svyq95SNh9GS-2gorv68GxWIYYeAShaG_UtTf8mvf6u-VIUr54Re2FoLc78ICR3nRhFH5D1_fNaP9hkMAHBqaJ8ATiq4d7-PfeXTCi0yY0qfWkGjuPtOC3IK7WmnEkiA5qUVpy0oHFPiqoAyNynWJRDFJka00JEpM1QFyF1Tz3PEGp0XlFnClY48iJG9UqXlDgaysnG3ro2sDm8ftva0IjX1Sp7Z9FyWQci-yOYfST00wKHQd7z5-Eo3cTd5M0BhcVXeR0gprdK1TTDLZLznFJQ36HYwrUFEXvTyme6vkZfNPRb0z8KPq5Gs7dujE_cw"};
var jwk_private = {
"kty":"RSA",
"kid":"juliet@capulet.lit",
"use":"enc",
"n":"t6Q8PWSi1dkJj9hTP8hNYFlvadM7DflW9mWepOJhJ66w7nyoK1gPNqFMSQRyO125Gp-TEkodhWr0iujjHVx7BcV0llS4w5ACGgPrcAd6ZcSR0-Iqom-QFcNP8Sjg086MwoqQU_LYywlAGZ21WSdS_PERyGFiNnj3QQlO8Yns5jCtLCRwLHL0Pb1fEv45AuRIuUfVcPySBWYnDyGxvjYGDSM-AqWS9zIQ2ZilgT-GqUmipg0XOC0Cc20rgLe2ymLHjpHciCKVAbY5-L32-lSeZO-Os6U15_aXrk9Gw8cPUaX1_I8sLGuSiVdt3C_Fn2PZ3Z8i744FPFGGcG1qs2Wz-Q",
"e":"AQAB",
"d":"GRtbIQmhOZtyszfgKdg4u_N-R_mZGU_9k7JQ_jn1DnfTuMdSNprTeaSTyWfSNkuaAwnOEbIQVy1IQbWVV25NY3ybc_IhUJtfri7bAXYEReWaCl3hdlPKXy9UvqPYGR0kIXTQRqns-dVJ7jahlI7LyckrpTmrM8dWBo4_PMaenNnPiQgO0xnuToxutRZJfJvG4Ox4ka3GORQd9CsCZ2vsUDmsXOfUENOyMqADC6p1M3h33tsurY15k9qMSpG9OX_IJAXmxzAh_tWiZOwk2K4yxH9tS3Lq1yX8C1EWmeRDkK2ahecG85-oLKQt5VEpWHKmjOi_gJSdSgqcN96X52esAQ",
"p":"2rnSOV4hKSN8sS4CgcQHFbs08XboFDqKum3sc4h3GRxrTmQdl1ZK9uw-PIHfQP0FkxXVrx-WE-ZEbrqivH_2iCLUS7wAl6XvARt1KkIaUxPPSYB9yk31s0Q8UK96E3_OrADAYtAJs-M3JxCLfNgqh56HDnETTQhH3rCT5T3yJws",
"q":"1u_RiFDP7LBYh3N4GXLT9OpSKYP0uQZyiaZwBtOCBNJgQxaj10RWjsZu0c6Iedis4S7B_coSKB0Kj9PaPaBzg-IySRvvcQuPamQu66riMhjVtG6TlV8CLCYKrYl52ziqK0E_ym2QnkwsUX7eYTB7LbAHRK9GqocDE5B0f808I4s",
"dp":"KkMTWqBUefVwZ2_Dbj1pPQqyHSHjj90L5x_MOzqYAJMcLMZtbUtwKqvVDq3tbEo3ZIcohbDtt6SbfmWzggabpQxNxuBpoOOf_a_HgMXK_lhqigI4y_kqS1wY52IwjUn5rgRrJ-yYo1h41KR-vz2pYhEAeYrhttWtxVqLCRViD6c",
"dq":"AvfS0-gRxvn0bwJoMSnFxYcK1WnuEjQFluMGfwGitQBWtfZ1Er7t1xDkbN9GQTB9yqpDoYaN06H7CFtrkxhJIBQaj6nkF5KKS3TQtQ5qCzkOkmxIe3KRbBymXxkb5qwUpX5ELD5xFc6FeiafWYY63TmmEAu_lRFCOJ3xDea-ots",
"qi":"lSQi-w9CpyUReMErP1RsBLk7wNtOvs5EQpPqmuMvqW57NBUczScEoPwmUqqabu9V0-Py4dQ57_bapoKRu1R90bvuFnU63SHWEFglZQvJDMeAvmj4sm-Fp0oYu_neotgQ0hzbI5gry7ajdYy9-2lNx_76aBZoOUu9HCJ-UsfSOI8"
};
var tags = {
SEQUENCE: 0x30,
INTEGER: 0x02,
BIT_STRING: 0x03,
OCTET_STRING: 0x04,
NULL: 0x05,
OBJECT_IDENTIFIER: 0x06,
SET: 0x11,
PrintableString: 0x13,
T61String: 0x14,
IA5String: 0x16,
UTCTime: 0x17,
};
QUnit.test("array to PEM", function(assert) {
var a = [1,2,3];
var p = arrayToPem(a);
var a2 = pemToArray(p);
assert.deepEqual(a2, a, "can you count?");
});
QUnit.test("array to String", function(assert) {
var a = "ssh-rsa".split('').map(function (c) {return c.charCodeAt();});
assert.equal(arrayToString(pemToArray(arrayToPem(a))), "ssh-rsa");
});
QUnit.test("lenToArray", function(assert) {
var a = 66051;
assert.deepEqual(lenToArray(a), [0, 1, 2, 3]);
});
QUnit.test("arrayToLen", function(assert) {
var a = [0, 1, 2, 3];
assert.deepEqual(arrayToLen(a), 66051);
});
public_ssh.forEach(function (public, index) {
QUnit.test("decoding ssh public key " + index, function(assert) {
var key = decodePublicKey(public);
assert.equal(key.type, "ssh-rsa", "type");
assert.equal(key.name, "name", "name");
if(index===0) {
assert.deepEqual(key.key, public_ssh_decoded[0].key, "key");
}
})
});
QUnit.test("Encoding ssh public key", function (assert) {
var result = encodePublicKey(jwk_public, "name");
assert.equal(result, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDc9Ynq4OwU+BCjruh/chjDRK/Kr3lI2H0ZL7aCiu/rwbFYhhh4BKFob9S1N/ya9/q75UhSvnhF7YWgtzvwgJHedGEUfkPX981o/2GQwAcGponwBOKrh3v4995dMKLTJjSp9aQaO4+04LcgrtaacSSIDmpRWnLSgcU+KqgDI3KdYlEMUmRrTQkSkzVAXIXVPPc8QanReUWcKVjjyIkb1SpeUOBrKycbeujawObx+29rQiNfVKntn0XJZByL7I5h9JPTTAodB3vPn4SjdxN3kzQGFxVd5HSCmt0rVNMMtkvOcUlDfodjCtQURe9PKZ7q+Rl809FvTPwo+rkazt26MT9z name");
});
QUnit.test("base64url", function (assert){
var result = stringToArray(base64urlDecode(jwk_public.n));
assert.equal(result.length, 256);
});
QUnit.test("high bit", function (assert) {
assert.deepEqual(checkHighestBit([0x80]), [0x00, 0x80]);
assert.deepEqual(checkHighestBit([0x0F]), [0x0F]);
});
QUnit.test("jwk", function (assert){
var sshkey = encodePublicKey(jwk_public, "name");
assert.deepEqual(stringToArray(base64urlDecode(jwk_public.e)), [1,0,1]);
assert.equal(sshkey, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDc9Ynq4OwU+BCjruh/chjDRK/Kr3lI2H0ZL7aCiu/rwbFYhhh4BKFob9S1N/ya9/q75UhSvnhF7YWgtzvwgJHedGEUfkPX981o/2GQwAcGponwBOKrh3v4995dMKLTJjSp9aQaO4+04LcgrtaacSSIDmpRWnLSgcU+KqgDI3KdYlEMUmRrTQkSkzVAXIXVPPc8QanReUWcKVjjyIkb1SpeUOBrKycbeujawObx+29rQiNfVKntn0XJZByL7I5h9JPTTAodB3vPn4SjdxN3kzQGFxVd5HSCmt0rVNMMtkvOcUlDfodjCtQURe9PKZ7q+Rl809FvTPwo+rkazt26MT9z name")
});
[{len:0x01, octet:[0x01]},
{len:0x104, octet:[0x01, 0x04]},
{len:0xFF32, octet:[0xFF,0x32]},
{len:0x1000000, octet:[1,0,0,0]},
{len:0x7FFFFFFF, octet:[0x7F,0xFF,0xFF,0xFF]}, //biggest one
].forEach(function (t) {
QUnit.test("Integer to Octet:" + t.len, function (assert) {
assert.deepEqual(integerToOctet(t.len), t.octet, t.len);
});
});
[{len: 0x34 , asn: [0x34]},
{len: 256 , asn: [0x80 + 2, 0x01, 0x00]},
{len: 0x134 , asn: [0x80 + 2, 0x01, 0x34]},
{len: 0x12345 , asn: [0x80 + 3, 0x01, 0x23, 0x45]},
{len: 0x123456 , asn: [0x80 + 3, 0x12, 0x34, 0x56]},
].forEach(function (t) {
QUnit.test("ASN.1 Len Writing:" + t.len, function (assert) {
assert.deepEqual(asnEncodeLen(t.len), t.asn, t.len);
});
});
QUnit.test("encodePrivateKey", function (assert) {
var encoded = encodePrivateKey(jwk_private);
console.log(encoded);
assert.ok(true);
});