diff --git a/defaults.env b/defaults.env index 6bdd8319c2..9ae2808b8c 100644 --- a/defaults.env +++ b/defaults.env @@ -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 diff --git a/gratipay/main.py b/gratipay/main.py index 31618d59d5..eaebc89818 100644 --- a/gratipay/main.py +++ b/gratipay/main.py @@ -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) diff --git a/gratipay/security/crypto.py b/gratipay/security/crypto.py index e4adf0fe78..4e45845d84 100644 --- a/gratipay/security/crypto.py +++ b/gratipay/security/crypto.py @@ -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 # ===== @@ -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 diff --git a/gratipay/wireup.py b/gratipay/wireup.py index b944633db3..26da247865 100644 --- a/gratipay/wireup.py +++ b/gratipay/wireup.py @@ -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 ( @@ -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.") @@ -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, diff --git a/requirements.txt b/requirements.txt index ef08117ba2..52c9b87362 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/tests/py/test_security.py b/tests/py/test_security.py index 88f0402b14..d34120156b 100644 --- a/tests/py/test_security.py +++ b/tests/py/test_security.py @@ -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 @@ -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') diff --git a/vendor/cffi-1.6.0.tar.gz b/vendor/cffi-1.6.0.tar.gz new file mode 100644 index 0000000000..941a17aa37 Binary files /dev/null and b/vendor/cffi-1.6.0.tar.gz differ diff --git a/vendor/cryptography-1.3.1.tar.gz b/vendor/cryptography-1.3.1.tar.gz new file mode 100644 index 0000000000..fdcfea336d Binary files /dev/null and b/vendor/cryptography-1.3.1.tar.gz differ diff --git a/vendor/enum34-1.1.4.tar.gz b/vendor/enum34-1.1.4.tar.gz new file mode 100644 index 0000000000..0112aca9c4 Binary files /dev/null and b/vendor/enum34-1.1.4.tar.gz differ diff --git a/vendor/idna-2.1.tar.gz b/vendor/idna-2.1.tar.gz new file mode 100644 index 0000000000..c028c715d2 Binary files /dev/null and b/vendor/idna-2.1.tar.gz differ diff --git a/vendor/ipaddress-1.0.16.tar.gz b/vendor/ipaddress-1.0.16.tar.gz new file mode 100644 index 0000000000..5f8428d496 Binary files /dev/null and b/vendor/ipaddress-1.0.16.tar.gz differ diff --git a/vendor/pycparser-2.14.tar.gz b/vendor/pycparser-2.14.tar.gz new file mode 100644 index 0000000000..6cdaab19aa Binary files /dev/null and b/vendor/pycparser-2.14.tar.gz differ