Created
October 22, 2016 05:46
-
-
Save kashizui/29e7e0a137ed180df506d960b06d3ada to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Simple secure voting system on top of KeyBase and Git. | |
You must run it from within a Git repository. | |
Usage: | |
keyvote.py vote <issue> <choice> | |
keyvote.py count <issue> | |
""" | |
from docopt import docopt | |
import os | |
import subprocess | |
import json | |
import shlex | |
import StringIO | |
from collections import Counter | |
import re | |
import sys | |
def shell(command, with_stderr=False): | |
if not isinstance(command, list): | |
command = shlex.split(command) | |
if with_stderr: | |
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
stdout, stderr = p.communicate() | |
if p.returncode != 0: | |
print >>sys.stderr, stderr | |
raise Exception(stderr) | |
return stdout, stderr | |
else: | |
return subprocess.check_output(command) | |
def current_username(): | |
out = shell('keybase status') | |
lines = out.split('\n') | |
key, value = map(str.strip, lines[0].split(':')) | |
assert key == 'Username' | |
return value | |
class GitVoter(object): | |
def __init__(self): | |
if not os.path.exists('.git'): | |
raise Exception("Must be run at root of Git repo") | |
@property | |
def usernames(self): | |
with open('_members', 'r') as f: | |
return sorted(map(str.strip, f.readlines())) | |
def _refresh(self): | |
shell('git pull') | |
# TODO: check integrity of repo: | |
# Have any votes been deleted from the repo? | |
# Are there any conflicts when pulling latest changes? | |
# (i.e. has anyone rewritten history?) | |
def vote(self, issue, choice): | |
self._refresh() | |
# Ensure directory exists for the issue | |
if not os.path.isdir(issue): | |
os.mkdir(issue) | |
for votefile in os.listdir(issue): | |
user = votefile.split('-')[0] | |
if user == current_username(): | |
raise Exception("You can't vote twice!") | |
vote = { | |
'issue': issue, | |
'choice': choice, | |
} | |
filename = '%s-%s' % (current_username(), os.urandom(16).encode('hex')) | |
votepath = os.path.join(issue, filename) | |
shell(['keybase', 'pgp', 'encrypt', '--sign', | |
'-o', votepath, | |
'-m', json.dumps(vote)] + self.usernames) | |
self._commit(issue, votepath) | |
def _commit(self, issue, votepath): | |
shell('git add ' + votepath) | |
shell(['git', 'commit', '-m', '%s voted in %s' % (current_username(), issue)]) | |
for _ in xrange(3): | |
try: | |
shell('git push') | |
return | |
except subprocess.CalledProcessError: | |
shell('git pull') | |
else: | |
raise Exception("Unable to resolve git conflicts") | |
def count(self, issue): | |
self._refresh() | |
if not os.path.isdir(issue): | |
raise Exception("Issue %s doesn't exist yet" % issue) | |
counts = Counter() | |
voters = set() | |
for votefile in os.listdir(issue): | |
votepath = os.path.join(issue, votefile) | |
user = votefile.split('-')[0] | |
vote = self._decrypt_vote(votepath, user) | |
counts[vote['choice']] += 1 | |
if user in voters: | |
raise Exception("Vote counted twice for %s!" % user) | |
voters.add(user) | |
return counts | |
def _decrypt_vote(self, path, user): | |
# This should throw an error if the signature is invalid! | |
out = shell(['keybase', 'pgp', 'decrypt', '--signed-by', user, '-i', path]) | |
return json.loads(out) | |
if __name__ == '__main__': | |
arguments = docopt(__doc__, version='KeyVote 0.0.1') | |
issue = arguments['<issue>'] | |
choice = arguments['<choice>'] | |
voter = GitVoter() | |
if arguments['vote']: | |
voter.vote(issue, choice) | |
elif arguments['count']: | |
for choice, count in voter.count(issue).iteritems(): | |
print '%s: %s' % (choice, count) | |
else: | |
print __doc__ | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment