Skip to content
This repository has been archived by the owner on Feb 8, 2018. It is now read-only.

Commit

Permalink
implement symmetric encryption
Browse files Browse the repository at this point in the history
  • Loading branch information
chadwhitacre committed May 1, 2016
1 parent 279b7ca commit 01b4ec3
Show file tree
Hide file tree
Showing 12 changed files with 72 additions and 0 deletions.
2 changes: 2 additions & 0 deletions defaults.env
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ PORT=8537
BASE_URL=http://localhost:8537
DATABASE_MAXCONN=10

CRYPTO_KEYS="1YrzmaGBUeFrwD9SpBOqv33a2ElGns9mUtldAmoU7hs="

GRATIPAY_ASSET_URL=/assets/
GRATIPAY_CACHE_STATIC=no
GRATIPAY_COMPRESS_ASSETS=no
Expand Down
1 change: 1 addition & 0 deletions gratipay/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
tell_sentry = website.tell_sentry = gratipay.wireup.make_sentry_teller(env)
website.db = gratipay.wireup.db(env)
website.mailer = gratipay.wireup.mail(env, website.project_root)
gratipay.wireup.crypto(env)
gratipay.wireup.base_url(website, env)
gratipay.wireup.secure_cookies(env)
gratipay.wireup.billing(env)
Expand Down
39 changes: 39 additions & 0 deletions gratipay/security/crypto.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from __future__ import absolute_import, division, print_function, unicode_literals

import hashlib
import json
import random
import string
import time

from cryptography.fernet import Fernet, MultiFernet


# utils
# =====
Expand Down Expand Up @@ -61,3 +64,39 @@ def constant_time_compare(val1, val2):
for x, y in zip(val1, val2):
result |= ord(x) ^ ord(y)
return result == 0


# Encrypting Packer
# =================

class EncryptingPacker(object):
"""Implement conversion of Python objects to/from encrypted bytestrings.
:param bytes key: a Fernet key as ``bytes``, for encryption and decryption via
``cryptography.fernet.MultiFernet``
:param list old_keys: additional Fernet keys as ``bytes`` for decryption via
``cryptography.fernet.MultiFernet``
"""

def __init__(self, key, *old_keys):
keys = [key] + list(old_keys)
self.fernet = MultiFernet([Fernet(k) for k in keys])

def pack(self, obj):
"""Given a JSON-serializable object, return an encrypted byte string.
"""
obj = json.dumps(obj) # serialize to unicode
obj = obj.encode('utf8') # convert to bytes
obj = self.fernet.encrypt(obj) # encrypt
return obj

def unpack(self, obj):
"""Given an encrypted byte string, return a Python object.
"""
if not type(obj) is bytes:
raise TypeError("need bytes, got {}".format(type(obj)))
obj = self.fernet.decrypt(obj) # decrypt
obj = obj.decode('utf8') # convert to unicode
obj = json.loads(obj) # deserialize from unicode
return obj
6 changes: 6 additions & 0 deletions gratipay/wireup.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from gratipay.models.participant import Participant
from gratipay.models.team import Team
from gratipay.models import GratipayDB
from gratipay.security.crypto import EncryptingPacker
from gratipay.utils.emails import compile_email_spt, ConsoleMailer
from gratipay.utils.http_caching import asset_etag
from gratipay.utils.i18n import (
Expand All @@ -59,6 +60,10 @@ def db(env):

return db

def crypto(env):
keys = [k.encode('ASCII') for k in env.crypto_keys.split()]
Participant.encrypting_packer = EncryptingPacker(*keys)

def mail(env, project_root='.'):
if env.aws_ses_access_key_id and env.aws_ses_secret_access_key and env.aws_ses_default_region:
aspen.log_dammit("AWS SES is configured! We'll send mail through SES.")
Expand Down Expand Up @@ -377,6 +382,7 @@ def env():
BASE_URL = unicode,
DATABASE_URL = unicode,
DATABASE_MAXCONN = int,
CRYPTO_KEYS = unicode,
GRATIPAY_ASSET_URL = unicode,
GRATIPAY_CACHE_STATIC = is_yesish,
GRATIPAY_COMPRESS_ASSETS = is_yesish,
Expand Down
6 changes: 6 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,9 @@

./vendor/boto3-1.3.0.tar.gz

./vendor/pycparser-2.14.tar.gz
./vendor/cffi-1.6.0.tar.gz
./vendor/enum34-1.1.4.tar.gz
./vendor/idna-2.1.tar.gz
./vendor/ipaddress-1.0.16.tar.gz
./vendor/cryptography-1.3.1.tar.gz
18 changes: 18 additions & 0 deletions tests/py/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

from aspen import Response
from aspen.http.request import Request
from base64 import urlsafe_b64decode
from gratipay import security
from gratipay.models.participant import Participant
from gratipay.testing import Harness
from pytest import raises

Expand Down Expand Up @@ -42,3 +44,19 @@ def test_ahtr_sets_x_content_type_options(self):
def test_ahtr_sets_x_xss_protection(self):
headers = self.client.GET('/about/').headers
assert headers['X-XSS-Protection'] == '1; mode=block'


# ep - EncryptingPacker

def test_ep_packs_encryptingly(self):
packed = Participant.encrypting_packer.pack({"foo": "bar"})
assert urlsafe_b64decode(packed)[0] == b'\x80' # Frenet version

def test_ep_unpacks_decryptingly(self):
packed = b'gAAAAABXJMbdriJ984uMCMKfQ5p2UUNHB1vG43K_uJyzUffbu2Uwy0d71kAnqOKJ7Ww_FEQz9Dliw8'\
b'7UpM5TdyoJsll5nMAicg=='
assert Participant.encrypting_packer.unpack(packed) == {"foo": "bar"}

def test_ep_demands_bytes(self):
raises(TypeError, Participant.encrypting_packer.unpack, buffer('buffer'))
raises(TypeError, Participant.encrypting_packer.unpack, 'unicode')
Binary file added vendor/cffi-1.6.0.tar.gz
Binary file not shown.
Binary file added vendor/cryptography-1.3.1.tar.gz
Binary file not shown.
Binary file added vendor/enum34-1.1.4.tar.gz
Binary file not shown.
Binary file added vendor/idna-2.1.tar.gz
Binary file not shown.
Binary file added vendor/ipaddress-1.0.16.tar.gz
Binary file not shown.
Binary file added vendor/pycparser-2.14.tar.gz
Binary file not shown.

0 comments on commit 01b4ec3

Please sign in to comment.