From 55562572ae66e7f7d8ccd83bdba455b0eb7b2d27 Mon Sep 17 00:00:00 2001 From: nathaniel smith Date: Wed, 15 Feb 2017 23:25:39 -0800 Subject: [PATCH 1/5] oof, get user creation working --- scripts/abc | 4 ++ scripts/create_keyfile.py | 14 ++++ ttadmin/users/models.py | 133 +++++++++++++++++++++++++++++++++++++- 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 scripts/abc create mode 100755 scripts/create_keyfile.py diff --git a/scripts/abc b/scripts/abc new file mode 100644 index 0000000..6f84751 --- /dev/null +++ b/scripts/abc @@ -0,0 +1,4 @@ +foobar +some stuff +yeahh +hm diff --git a/scripts/create_keyfile.py b/scripts/create_keyfile.py new file mode 100755 index 0000000..1ae11ed --- /dev/null +++ b/scripts/create_keyfile.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +import sys + +KEYFILE_PATH = '/home/{}/.ssh/authorized_keys2' + + +def main(argv): + username = argv[1] + with open(KEYFILE_PATH.format(username), 'w') as f: + f.write(sys.stdin.read()) + + +if __name__ == '__main__': + exit(main(sys.argv)) diff --git a/ttadmin/users/models.py b/ttadmin/users/models.py index af1b6cc..267e7e0 100644 --- a/ttadmin/users/models.py +++ b/ttadmin/users/models.py @@ -1,7 +1,10 @@ +import os import re +from subprocess import run, CalledProcessError, PIPE +from tempfile import TemporaryFile from django.db.models import Model -from django.db.models.signals import pre_save +from django.db.models.signals import pre_save, post_save, post_delete from django.dispatch import receiver from django.contrib.auth.models import User from django.db.models import TextField, BooleanField, CharField, ForeignKey @@ -15,6 +18,23 @@ SSH_TYPE_CHOICES = ( ('ssh-dss', 'ssh-dss',), ) +KEYFILE_HEADER = """Hi! This file is automatically managed by tilde.town. You +probably shouldn't change it. If you want to add more public keys that's +totally fine: you can put them in ~/.ssh/authorized_keys""" +TMP_PATH = '/tmp/ttadmin' + +ENSURE_PRESENT = 'present' +ENSURE_ABSENT = 'absent' + +def user_in_passwd(username): + """Given a username, returns either the user's line in passwd or None. + Opens and reads passwd every time. Memoize or something if this becomes an + issue.""" + with open('/etc/passwd') as passwd: + for line in passwd: + if username == line.split(':')[0]: + return line.rstrip() + class Townie(User): """Both an almost normal Django User as well as an abstraction over a @@ -44,6 +64,58 @@ class Townie(User): self.username, self.email)) + # managing concrete system state + + def reconcile(self, ensure): + assert(self.reviewed) + dot_ssh_path = '/home/{}/.ssh'.format(self.username) + if ensure == ENSURE_ABSENT: + # TODO delete + # This is manual for now because it very rarely comes up and I want + # the present case to work. + pass + + if ensure == ENSURE_PRESENT: + # TODO handle rename case either with update fields or a rename action + # Add the user + result = run(['sudo', + 'adduser', + '--quiet', + '--shell={}'.format(self.shell), + '--gecos="{}"'.format(self.displayname), + '--disabled-password', + self.username,], + check=True, + ) + + # Create .ssh + run(['sudo', '--user={}'.format(self.username), 'mkdir', dot_ssh_path]) + + # Write out authorized_keys file + with TemporaryFile(dir="/tmp") as fp: + fp.write(self.generate_authorized_keys().encode('utf-8')) + fp.seek(0) + run(['sudo', + '--user={}'.format(self.username), + '/opt/bin/create_keyfile.py', + self.username], + stdin=fp + ) + + def generate_authorized_keys(self): + """returns a string suitable for writing out to an authorized_keys + file""" + content = KEYFILE_HEADER + pubkeys = Pubkey.objects.filter(townie=self) + for key in pubkeys: + if key.startswith('ssh-'): + content += '\n {}'.format(key.key) + else: + content += '\n{} {}'.format(key.key_type, key.key) + + return content + + class Pubkey(Model): key_type = CharField(max_length=50, blank=False, @@ -62,3 +134,62 @@ def on_townie_pre_save(sender, instance, **kwargs): if not existing[0].reviewed and instance.reviewed == True: instance.send_welcome_email() + +@receiver(post_save, sender=Townie) +def post_save_reconcile(sender, instance, **kwargs): + if not instance.reviewed: + return + instance.reconcile(ENSURE_PRESENT) + +@receiver(post_delete, sender=Townie) +def post_delete_reconcile(sender, instance, **kwargs): + if not instance.reviewed: + # TODO should i actually do this check? + # I might want to make it such that users can never become un-reviewed. + return + instance.reconcile(ENSURE_ABSENT) + +# what the puppet module does: +# * creates user account +# * creates home directory +# * creates authorized_keys2 +# * adds user to group 'tilde' (why?) +# * sets shell +# * creates .ssh directory +# * creates .irssi directory +# * creates templatized .irssi config file (irssi isn't even default anymore...) +# * creates hardcoded .twurlrc file (why not skel?) +# +# some of this stuff is pointless and the actually required stuff is: +# * create user account (useradd) +# * create home dir (useradd) +# * create .ssh/authorized_keys2 (need functions for this +# * set shell (chsh) +# * create .twurlrc (just use /etc/skel) + +# other things to consider: +# * what happens when a user wants their name changed? +# * it looks like usermod -l and a mv of the home dir can change a user's username. +# * would hook this into the pre_save signal to note a username change +# * what happens when a user is marked as not reviewed? +# * does this signal user deletion? Or does literal Townie deletion signal +# "needs to be removed from disk"? I think it makes the most sense for the +# latter to imply full user deletion. +# * I honestly can't even think of a reason to revert a user to "not reviewed" +# and perhaps it's best to just not make that possible. for now, though, I +# think I can ignore it. +# * what happens when a user needs to be banned? +# * the Townie should be deleted via post_delete signal +# * what are things about a user that might change in django and require changes on disk? +# * username +# * displayname (only if i start using this?) +# * ssh key +# +# how should this code be structured? +# * within the Townie model, hardcoded +# * outside the Townie model, procedurally +# * within an abstract class +# +# for now my gut says to implement stuff hardcoded in the Townie class but with +# an eye towards generalizing the pattern in some base class for other +# resources as needed. From d0c79a515939bc15d1e1d1629d22e82c75fd38a4 Mon Sep 17 00:00:00 2001 From: nathaniel smith Date: Thu, 16 Feb 2017 00:49:26 -0800 Subject: [PATCH 2/5] clean up user creation automation --- README.md | 24 +++++--- scripts/create_keyfile.py | 3 + serversetup.md | 6 +- setup.py | 5 +- ttadmin/users/models.py | 122 ++++++++++++++++++-------------------- 5 files changed, 84 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 9e78093..1525bcc 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,27 @@ _Being an adminstrative and user-signup tool for [https://tilde.town]_. -## Features +## Features, present and future - * User signup form (✓) - * with client-side key generation (only server-side key **validation** at this point) - * Guestbook (✓) - * Helpdesk (✓) - * User account management (admin only) + * (✓) User signup form + * eventually with client-side key generation (only server-side key + **validation** at this point) + * (✓) Guestbook + * (✓) Helpdesk + * (✓) User account management * Start/stop services * Cost reporting using AWS * Status monitoring -## Libraries +## Requirements - * Django 1.10 - * Python 3.4+ + * Python 3.5+ + * PostgreSQL 9+ + +## Installation / setup + +Refer to the rather rough [serversetup.md](server setup guide). just ask +~vilmibm though if you want to set this up. ## Authors diff --git a/scripts/create_keyfile.py b/scripts/create_keyfile.py index 1ae11ed..33be19b 100755 --- a/scripts/create_keyfile.py +++ b/scripts/create_keyfile.py @@ -1,4 +1,7 @@ #!/usr/bin/env python3 +"""this script allows django to add public keys for a user. it's in its own +script so that a specific command can be added to the ttadmin user's sudoers +file.""" import sys KEYFILE_PATH = '/home/{}/.ssh/authorized_keys2' diff --git a/serversetup.md b/serversetup.md index aef158e..8a87e58 100644 --- a/serversetup.md +++ b/serversetup.md @@ -5,7 +5,7 @@ * autoconf? * python3-dev * postgresql-server-dev-9.5 -* python 3.4+ +* python 3.5+ * postgresql * virtualenv * nginx @@ -14,9 +14,11 @@ * create ttadmin user * ttadmin db user (or just rely on ident..?) / database created +* copy `create_keyfile.py` from `scripts/` and put it in `/opt/bin/`. +* `chmod o+x /opt/bin/create_keyfile.py`` * add to sudoers: - ttadmin ALL=(ALL)NOPASSWD:/usr/sbin/adduser,/usr/sbin/deluser,/usr/sbin/delgroup + ttadmin ALL=(ALL)NOPASSWD:/usr/sbin/adduser,/bin/mkdir,/opt/bin/create_keyfile.py * have virtualenv with python 3.5+ ready, install tildetown-admin package into it * run django app as wsgi container through gunicorn as the ttadmin user with venv active diff --git a/setup.py b/setup.py index 68f1f0c..f1b0d30 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup setup( name='tildetown-admin', - version='1.0.0', + version='1.1.0', description='administrative webapp for tilde.town', url='https://github.com/nathanielksmith/prosaic', author='vilmibm shaksfrpease', @@ -18,6 +18,7 @@ setup( install_requires = ['Django==1.10.2', 'sshpubkeys==2.2.0', 'psycopg2==2.6.2', - 'requests==2.12.5'], + 'requests==2.12.5', + 'gunicorn==19.6.0'], include_package_data = True, ) diff --git a/ttadmin/users/models.py b/ttadmin/users/models.py index 267e7e0..308a9ce 100644 --- a/ttadmin/users/models.py +++ b/ttadmin/users/models.py @@ -1,10 +1,10 @@ import os import re -from subprocess import run, CalledProcessError, PIPE +from subprocess import run, CalledProcessError from tempfile import TemporaryFile from django.db.models import Model -from django.db.models.signals import pre_save, post_save, post_delete +from django.db.models.signals import pre_save from django.dispatch import receiver from django.contrib.auth.models import User from django.db.models import TextField, BooleanField, CharField, ForeignKey @@ -18,22 +18,10 @@ SSH_TYPE_CHOICES = ( ('ssh-dss', 'ssh-dss',), ) -KEYFILE_HEADER = """Hi! This file is automatically managed by tilde.town. You -probably shouldn't change it. If you want to add more public keys that's -totally fine: you can put them in ~/.ssh/authorized_keys""" -TMP_PATH = '/tmp/ttadmin' - -ENSURE_PRESENT = 'present' -ENSURE_ABSENT = 'absent' - -def user_in_passwd(username): - """Given a username, returns either the user's line in passwd or None. - Opens and reads passwd every time. Memoize or something if this becomes an - issue.""" - with open('/etc/passwd') as passwd: - for line in passwd: - if username == line.split(':')[0]: - return line.rstrip() +KEYFILE_HEADER = """########## GREETINGS! ########## +# Hi! This file is automatically managed by tilde.town. You +# probably shouldn't change it. If you want to add more public keys that's +# totally fine: you can put them in ~/.ssh/authorized_keys""" class Townie(User): @@ -66,50 +54,56 @@ class Townie(User): # managing concrete system state - def reconcile(self, ensure): + def create_on_disk(self): + """A VERY NOT IDEMPOTENT create function. Originally, I had ambitions + to have this be idempotent and able to incrementally update a user as + needed, but decided that was overkill for now.""" assert(self.reviewed) dot_ssh_path = '/home/{}/.ssh'.format(self.username) - if ensure == ENSURE_ABSENT: - # TODO delete - # This is manual for now because it very rarely comes up and I want - # the present case to work. - pass - if ensure == ENSURE_PRESENT: - # TODO handle rename case either with update fields or a rename action - # Add the user - result = run(['sudo', - 'adduser', - '--quiet', - '--shell={}'.format(self.shell), - '--gecos="{}"'.format(self.displayname), - '--disabled-password', - self.username,], - check=True, + _guarded_run(['sudo', + 'adduser', + '--quiet', + '--shell={}'.format(self.shell), + '--gecos="{}"'.format(self.displayname), + '--disabled-password', + self.username,]) + + # Create .ssh + _guarded_run(['sudo', + '--user={}'.format(self.username), + 'mkdir', + dot_ssh_path]) + + # Write out authorized_keys file + # Why is this a call out to a python script? There's no secure way with + # sudoers to allow this code to write to a file; if this code was to be + # compromised, the ability to write arbitrary files with sudo is a TKO. + # By putting the ssh key file creation into its own script, we can just + # give sudo access for that one command to this code. + # + # We could put the other stuff from here into that script and then only + # grant sudo for the script, but then we're moving code out of this + # virtual-env contained, maintainable thing into a script. it's my + # preference to have the script be as minimal as possible. + with TemporaryFile(dir="/tmp") as fp: + fp.write(self.generate_authorized_keys().encode('utf-8')) + fp.seek(0) + _guarded_run(['sudo', + '--user={}'.format(self.username), + '/opt/bin/create_keyfile.py', + self.username], + stdin=fp, ) - # Create .ssh - run(['sudo', '--user={}'.format(self.username), 'mkdir', dot_ssh_path]) - - # Write out authorized_keys file - with TemporaryFile(dir="/tmp") as fp: - fp.write(self.generate_authorized_keys().encode('utf-8')) - fp.seek(0) - run(['sudo', - '--user={}'.format(self.username), - '/opt/bin/create_keyfile.py', - self.username], - stdin=fp - ) - def generate_authorized_keys(self): """returns a string suitable for writing out to an authorized_keys file""" content = KEYFILE_HEADER pubkeys = Pubkey.objects.filter(townie=self) for key in pubkeys: - if key.startswith('ssh-'): - content += '\n {}'.format(key.key) + if key.key.startswith('ssh-'): + content += '\n{}'.format(key.key) else: content += '\n{} {}'.format(key.key_type, key.key) @@ -133,21 +127,23 @@ def on_townie_pre_save(sender, instance, **kwargs): return if not existing[0].reviewed and instance.reviewed == True: + instance.create_on_disk() instance.send_welcome_email() -@receiver(post_save, sender=Townie) -def post_save_reconcile(sender, instance, **kwargs): - if not instance.reviewed: - return - instance.reconcile(ENSURE_PRESENT) +def _guarded_run(cmd_args, **run_args): + try: + run(cmd_args, + check=True, + **run_args) + except CalledProcessError as e: + Ticket.objects.create(name='system', + email='root@tilde.town', + issue_type='other', + issue_text='error while running {}: {}'.format( + cmd_args, e)) -@receiver(post_delete, sender=Townie) -def post_delete_reconcile(sender, instance, **kwargs): - if not instance.reviewed: - # TODO should i actually do this check? - # I might want to make it such that users can never become un-reviewed. - return - instance.reconcile(ENSURE_ABSENT) + +# development notes! # what the puppet module does: # * creates user account From e2447b1499da31c146bbb1062e6aeedae2f10a8b Mon Sep 17 00:00:00 2001 From: nathaniel smith Date: Thu, 16 Feb 2017 00:52:08 -0800 Subject: [PATCH 3/5] remove extraneous file --- scripts/abc | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 scripts/abc diff --git a/scripts/abc b/scripts/abc deleted file mode 100644 index 6f84751..0000000 --- a/scripts/abc +++ /dev/null @@ -1,4 +0,0 @@ -foobar -some stuff -yeahh -hm From a2c609b75c3bd11352c98d3f9f1916dd4fbeac1c Mon Sep 17 00:00:00 2001 From: nathaniel smith Date: Thu, 16 Feb 2017 23:19:40 -0800 Subject: [PATCH 4/5] fix broken link --- ttadmin/users/templates/users/signup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ttadmin/users/templates/users/signup.html b/ttadmin/users/templates/users/signup.html index 3ba1497..75e77ea 100644 --- a/ttadmin/users/templates/users/signup.html +++ b/ttadmin/users/templates/users/signup.html @@ -29,7 +29,7 @@

If you have any problems with this process, please send a tweet to @tildetown - or file a helpdesk ticket. + or file a helpdesk ticket.

From 4ff5569dd7dc7c0a0fafd2b0b5f864ea46d3c5f3 Mon Sep 17 00:00:00 2001 From: nathaniel smith Date: Wed, 22 Feb 2017 00:45:31 -0800 Subject: [PATCH 5/5] first pass on MAGIC KEY MACHINE --- ttadmin/common/static/common/vt323.ttf | Bin 0 -> 153116 bytes ttadmin/users/static/users/base64url.js | 28 ++++ ttadmin/users/static/users/js-keygen-ui.js | 52 ++++++++ ttadmin/users/static/users/js-keygen.js | 53 ++++++++ ttadmin/users/static/users/ssh-util.js | 122 ++++++++++++++++++ ttadmin/users/templates/users/keymachine.html | 100 ++++++++++++++ ttadmin/users/urls.py | 3 +- ttadmin/users/views.py | 3 + 8 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 ttadmin/common/static/common/vt323.ttf create mode 100644 ttadmin/users/static/users/base64url.js create mode 100644 ttadmin/users/static/users/js-keygen-ui.js create mode 100644 ttadmin/users/static/users/js-keygen.js create mode 100644 ttadmin/users/static/users/ssh-util.js create mode 100644 ttadmin/users/templates/users/keymachine.html diff --git a/ttadmin/common/static/common/vt323.ttf b/ttadmin/common/static/common/vt323.ttf new file mode 100644 index 0000000000000000000000000000000000000000..afa69098f15bf64849d397f133d9467486904053 GIT binary patch literal 153116 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 literal 0 HcmV?d00001 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'