first pass on MAGIC KEY MACHINE
parent
a2c609b75c
commit
4ff5569dd7
Binary file not shown.
|
@ -0,0 +1,28 @@
|
||||||
|
/*jslint browser: true, sloppy: true */
|
||||||
|
//adapted from https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-08#appendix-C
|
||||||
|
|
||||||
|
function base64urlEncode(arg) {
|
||||||
|
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: // No pad chars in this case
|
||||||
|
break;
|
||||||
|
case 2: // Two pad chars
|
||||||
|
s += "==";
|
||||||
|
break;
|
||||||
|
case 3: // One pad char
|
||||||
|
s += "=";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw "Illegal base64url string!";
|
||||||
|
}
|
||||||
|
return window.atob(s); // Standard base64 decoder
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*jslint browser: true, sloppy: true, vars: true, indent: 2*/
|
||||||
|
var console, 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 (event) {
|
||||||
|
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'));
|
||||||
|
|
||||||
|
document.querySelector('#generate').addEventListener('click', function (event) {
|
||||||
|
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);
|
||||||
|
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];
|
||||||
|
document.querySelector('#result').style.display = "block";
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*jslint browser: true, devel: true, sloppy: true, vars: true*/
|
||||||
|
/*globals Uint8Array, Promise */
|
||||||
|
var extractable = true;
|
||||||
|
var encodePrivateKey, encodePublicKey;
|
||||||
|
|
||||||
|
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 = '', i;
|
||||||
|
var bytes = new Uint8Array(buffer);
|
||||||
|
var len = bytes.byteLength;
|
||||||
|
for (i = 0; i < len; i += 1) {
|
||||||
|
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 privateKey = window.crypto.subtle.exportKey(
|
||||||
|
"jwk",
|
||||||
|
key.privateKey
|
||||||
|
).then(encodePrivateKey).then(wrap).then(rsaPrivateKey);
|
||||||
|
|
||||||
|
var publicKey = window.crypto.subtle.exportKey(
|
||||||
|
"jwk",
|
||||||
|
key.publicKey
|
||||||
|
).then(function (jwk) {
|
||||||
|
return encodePublicKey(jwk, name);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all([privateKey, publicKey]);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
/*jslint browser: true, devel: true, bitwise: true, sloppy: true, vars: true*/
|
||||||
|
|
||||||
|
var base64urlDecode;
|
||||||
|
|
||||||
|
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 += 1) {
|
||||||
|
result = result * 256 + a[i];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function integerToOctet(n) {
|
||||||
|
var result = [];
|
||||||
|
for (true; 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 += 1) {
|
||||||
|
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 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) {
|
||||||
|
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);
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<!--
|
||||||
|
IMPORTANT! all of the keygen stuff on this page is adapted from
|
||||||
|
https://github.com/PatrickRoumanoff/js-keygen , without which it wouldn't
|
||||||
|
have happened.
|
||||||
|
-->
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>tilde.town magic key machine</title>
|
||||||
|
<script src="{% static 'users/base64url.js' %}"></script>
|
||||||
|
<script src="{% static 'users/ssh-util.js' %}"></script>
|
||||||
|
<script src="{% static 'users/js-keygen-ui.js' %}"></script>
|
||||||
|
<script src="{% static 'users/js-keygen.js' %}"></script>
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: "vt323";
|
||||||
|
src: url("{% static 'common/vt323.ttf' %}");
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #871B51;
|
||||||
|
font-family: "vt323";
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size:400%;
|
||||||
|
}
|
||||||
|
.preamble {
|
||||||
|
font-size: 200%;
|
||||||
|
background-color: #9AB211;
|
||||||
|
text-align: justify;
|
||||||
|
padding-left:10%;
|
||||||
|
padding-right:10%;
|
||||||
|
|
||||||
|
}
|
||||||
|
#generate {
|
||||||
|
font-size:200%;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: rgb(224, 176, 255);
|
||||||
|
}
|
||||||
|
.key {
|
||||||
|
display: none;
|
||||||
|
background-color: #9AB211;
|
||||||
|
}
|
||||||
|
.key a {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>THE <a href="https://tilde.town">TILDE.TOWN</a> MAGIC KEY MACHINE</h1>
|
||||||
|
<div class="preamble">this page will make you an SSH keypair. a keypair
|
||||||
|
consists of a <em>public</em> key and a <em>private</em> key. they're
|
||||||
|
actually really long numbers that are used in some insane math which all
|
||||||
|
boils down to one really cool fact:</div>
|
||||||
|
<div class="preamble">
|
||||||
|
<em>
|
||||||
|
<strong>
|
||||||
|
when used together, your keypair lets your computer talk, in
|
||||||
|
perfect secrecy, with another computer.
|
||||||
|
</strong>
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
<button id="generate"><em>-> MACHINE, CLANK AWAY FOR ME! <-</em></button>
|
||||||
|
<div id="publickey" class="key">
|
||||||
|
<h2>I'm a public key!</h2>
|
||||||
|
<a class="save" download="id_rsa.pub"><button>save me to a file</button></a>
|
||||||
|
<br>
|
||||||
|
<textarea rows="10" cols="40"></textarea>
|
||||||
|
</div>
|
||||||
|
<div id="privatekey" class="key">
|
||||||
|
<h2>I'm a private key! <br><em>KEEP ME SECRET, PLEASE</em></h2>
|
||||||
|
<a class="save" download="id_rsa"><button>save me to a file</button></a>
|
||||||
|
<br>
|
||||||
|
<textarea rows="10" cols="40"></textarea>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
$ = document.querySelector.bind(document);
|
||||||
|
var button = $('#generate');
|
||||||
|
var pub_ta = $('#publickey textarea');
|
||||||
|
var priv_ta = $('#privatekey textarea');
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
generateKeyPair(null, null, 'someone@tilde.town').then(function (keys) {
|
||||||
|
var private_key = keys[0]
|
||||||
|
var public_key = keys[1]
|
||||||
|
pub_ta.textContent = public_key;
|
||||||
|
priv_ta.textContent = private_key;
|
||||||
|
$('#publickey').style.display = 'block';
|
||||||
|
$('#privatekey').style.display = 'block';
|
||||||
|
$('#publickey a.save').href = buildHref(public_key);
|
||||||
|
$('#privatekey a.save').href = buildHref(private_key);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from .views import SignupView, ThanksView
|
from .views import SignupView, ThanksView, KeyMachineView
|
||||||
|
|
||||||
app_name = 'users'
|
app_name = 'users'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^signup/?$', SignupView.as_view(), name='signup'),
|
url(r'^signup/?$', SignupView.as_view(), name='signup'),
|
||||||
url(r'^thanks/?$', ThanksView.as_view(), name='thanks'),
|
url(r'^thanks/?$', ThanksView.as_view(), name='thanks'),
|
||||||
|
url(r'^keymachine/?$', KeyMachineView.as_view(), name='keymachine'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -34,3 +34,6 @@ class SignupView(FormView):
|
||||||
|
|
||||||
class ThanksView(TemplateView):
|
class ThanksView(TemplateView):
|
||||||
template_name = 'users/thanks.html'
|
template_name = 'users/thanks.html'
|
||||||
|
|
||||||
|
class KeyMachineView(TemplateView):
|
||||||
|
template_name = 'users/keymachine.html'
|
||||||
|
|
Loading…
Reference in New Issue