-
Notifications
You must be signed in to change notification settings - Fork 308
implement symmetric encryption #3998
Changes from all commits
01b4ec3
2a6bbd4
6edce65
da4cf0f
e0dc3c4
049bd48
34ba9cd
5ce0ff8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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()) |
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 |
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 | ||
# ===== | ||
|
@@ -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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't this be simplified to just take There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,15 @@ | ||
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 cryptography.fernet import Fernet, InvalidToken | ||
from gratipay import security | ||
from gratipay.models.participant import Participant | ||
from gratipay.security.crypto import EncryptingPacker | ||
from gratipay.testing import Harness | ||
from pytest import raises | ||
|
||
|
@@ -42,3 +49,34 @@ 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"} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be worth checking whether |
||
|
||
def test_ep_fails_to_unpack_old_data_with_a_new_key(self): | ||
encrypting_packer = EncryptingPacker(Fernet.generate_key()) | ||
raises(InvalidToken, encrypting_packer.unpack, self.packed) | ||
|
||
def test_ep_can_unpack_if_old_key_is_provided(self): | ||
old_key = str(self.client.website.env.crypto_keys) | ||
encrypting_packer = EncryptingPacker(Fernet.generate_key(), old_key) | ||
assert encrypting_packer.unpack(self.packed) == {"foo": "bar"} | ||
|
||
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') |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?