diff --git a/ttadmin/common/static/common/vt323.ttf b/ttadmin/common/static/common/vt323.ttf new file mode 100644 index 0000000..afa6909 Binary files /dev/null and b/ttadmin/common/static/common/vt323.ttf differ diff --git a/ttadmin/users/static/users/base64url.js b/ttadmin/users/static/users/base64url.js new file mode 100644 index 0000000..8a35147 --- /dev/null +++ b/ttadmin/users/static/users/base64url.js @@ -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 +} diff --git a/ttadmin/users/static/users/js-keygen-ui.js b/ttadmin/users/static/users/js-keygen-ui.js new file mode 100644 index 0000000..3e2a894 --- /dev/null +++ b/ttadmin/users/static/users/js-keygen-ui.js @@ -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); + }); + }); +}); diff --git a/ttadmin/users/static/users/js-keygen.js b/ttadmin/users/static/users/js-keygen.js new file mode 100644 index 0000000..17e442b --- /dev/null +++ b/ttadmin/users/static/users/js-keygen.js @@ -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]); + }); +} diff --git a/ttadmin/users/static/users/ssh-util.js b/ttadmin/users/static/users/ssh-util.js new file mode 100644 index 0000000..bf261cb --- /dev/null +++ b/ttadmin/users/static/users/ssh-util.js @@ -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); +} diff --git a/ttadmin/users/templates/users/keymachine.html b/ttadmin/users/templates/users/keymachine.html new file mode 100644 index 0000000..2e03812 --- /dev/null +++ b/ttadmin/users/templates/users/keymachine.html @@ -0,0 +1,100 @@ +{% load static %} + + + + + + tilde.town magic key machine + + + + + + + +

THE TILDE.TOWN MAGIC KEY MACHINE

+
this page will make you an SSH keypair. a keypair + consists of a public key and a private key. they're + actually really long numbers that are used in some insane math which all + boils down to one really cool fact:
+
+ + + when used together, your keypair lets your computer talk, in + perfect secrecy, with another computer. + + +
+ +
+

I'm a public key!

+ +
+ +
+
+

I'm a private key!
KEEP ME SECRET, PLEASE

+ +
+ +
+ + + + diff --git a/ttadmin/users/urls.py b/ttadmin/users/urls.py index b11c6d3..5832263 100644 --- a/ttadmin/users/urls.py +++ b/ttadmin/users/urls.py @@ -1,9 +1,10 @@ from django.conf.urls import url -from .views import SignupView, ThanksView +from .views import SignupView, ThanksView, KeyMachineView app_name = 'users' urlpatterns = [ url(r'^signup/?$', SignupView.as_view(), name='signup'), url(r'^thanks/?$', ThanksView.as_view(), name='thanks'), + url(r'^keymachine/?$', KeyMachineView.as_view(), name='keymachine'), ] diff --git a/ttadmin/users/views.py b/ttadmin/users/views.py index 4f4b5f4..f5cbe39 100644 --- a/ttadmin/users/views.py +++ b/ttadmin/users/views.py @@ -34,3 +34,6 @@ class SignupView(FormView): class ThanksView(TemplateView): template_name = 'users/thanks.html' + +class KeyMachineView(TemplateView): + template_name = 'users/keymachine.html'