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

implement symmetric encryption #3998

Merged
merged 8 commits into from
May 10, 2016
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bin/keygen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env python2
from __future__ import absolute_import, division, print_function, unicode_literals
from cryptography.fernet import Fernet
print(Fernet.generate_key())
10 changes: 10 additions & 0 deletions bin/rekey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env python2
from __future__ import absolute_import, division, print_function, unicode_literals

from gratipay import wireup

env = wireup.env()
db = wireup.db(env)
wireup.crypto(env)

print("{} record(s) rekeyed.".format(0)) # stubbed until we have something to rekey
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="
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe leave a comment here saying that extra keys should be separated by a space?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, in a space separated list - which is the latest key (the one with which new items will be encrypted)? The first or the last?


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
47 changes: 47 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,47 @@ 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 str key: a `Fernet`_ key to use for encryption and decryption
:param list old_keys: additional `Fernet`_ keys to use for decryption

.. note::

Encrypted messages contain the timestamp at which they were generated
*in plaintext*. See `our audit`_ for discussion of this and other
considerations with `Fernet`_.

.. _Fernet: https://cryptography.io/en/latest/fernet/
.. _our audit: https://github.com/gratipay/gratipay.com/pull/3998#issuecomment-216227070

"""

def __init__(self, key, *old_keys):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this be simplified to just take *keys?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, maybe this was done to emphasize the answer to https://github.com/gratipay/gratipay.com/pull/3998/files#r62548400?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's more or less what I was thinking. I wanted to differentiate between the first key, which is the one used for encryption (and decryption), and the old keys, which can only be used for decryption.

keys = [key] + list(old_keys)
self.fernet = MultiFernet([Fernet(k) for k in keys])

def pack(self, obj):
"""Given a JSON-serializable object, return a `Fernet`_ token.
"""
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, token):
"""Given a `Fernet`_ token with JSON in the ciphertext, return a Python object.
"""
obj = token
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.2.tar.gz
27 changes: 27 additions & 0 deletions tests/py/test_security.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
from __future__ import absolute_import, division, print_function, unicode_literals

import struct
import datetime

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 +47,25 @@ 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

packed = b'gAAAAABXJMbdriJ984uMCMKfQ5p2UUNHB1vG43K_uJyzUffbu2Uwy0d71kAnqOKJ7Ww_FEQz9Dliw87UpM'\
b'5TdyoJsll5nMAicg=='

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

def test_ep_unpacks_decryptingly(self):
assert Participant.encrypting_packer.unpack(self.packed) == {"foo": "bar"}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth checking whether encrypting_packer decrypts data encrypted with old keys too?


def test_ep_leaks_timestamp_derp(self):
# https://github.com/pyca/cryptography/issues/2714
timestamp, = struct.unpack(">Q", urlsafe_b64decode(self.packed)[1:9]) # unencrypted!
assert datetime.datetime.fromtimestamp(timestamp).year == 2016

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.2.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.