Skip to content

Instantly share code, notes, and snippets.

@kashizui
Created October 22, 2016 05:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kashizui/29e7e0a137ed180df506d960b06d3ada to your computer and use it in GitHub Desktop.
Save kashizui/29e7e0a137ed180df506d960b06d3ada to your computer and use it in GitHub Desktop.
"""
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