Alex Gaynor | 02fad00 | 2013-10-30 14:16:13 -0700 | [diff] [blame] | 1 | import base64 |
| 2 | import os |
| 3 | import struct |
| 4 | import time |
| 5 | |
Alex Gaynor | bbeba71 | 2013-10-30 14:29:58 -0700 | [diff] [blame] | 6 | import six |
| 7 | |
Alex Gaynor | 02fad00 | 2013-10-30 14:16:13 -0700 | [diff] [blame] | 8 | from cryptography.hazmat.primitives import padding, hashes |
| 9 | from cryptography.hazmat.primitives.hmac import HMAC |
| 10 | from cryptography.hazmat.primitives.block import BlockCipher, ciphers, modes |
| 11 | |
| 12 | |
Alex Gaynor | 38f3455 | 2013-10-31 14:50:00 -0700 | [diff] [blame] | 13 | class InvalidToken(Exception): |
| 14 | pass |
| 15 | |
| 16 | |
Alex Gaynor | 02fad00 | 2013-10-30 14:16:13 -0700 | [diff] [blame] | 17 | class Fernet(object): |
| 18 | def __init__(self, key): |
| 19 | super(Fernet, self).__init__() |
Alex Gaynor | cd47c4a | 2013-10-31 09:46:27 -0700 | [diff] [blame] | 20 | assert len(key) == 32 |
Alex Gaynor | 02fad00 | 2013-10-30 14:16:13 -0700 | [diff] [blame] | 21 | self.signing_key = key[:16] |
| 22 | self.encryption_key = key[16:] |
| 23 | |
| 24 | def encrypt(self, data): |
| 25 | current_time = int(time.time()) |
| 26 | iv = os.urandom(16) |
| 27 | return self._encrypt_from_parts(data, current_time, iv) |
| 28 | |
| 29 | def _encrypt_from_parts(self, data, current_time, iv): |
Alex Gaynor | 38f3455 | 2013-10-31 14:50:00 -0700 | [diff] [blame] | 30 | if isinstance(data, six.text_type): |
Alex Gaynor | c1ea0a0 | 2013-10-31 15:03:53 -0700 | [diff] [blame^] | 31 | raise TypeError( |
| 32 | "Unicode-objects must be encoded before encryption" |
| 33 | ) |
Alex Gaynor | 38f3455 | 2013-10-31 14:50:00 -0700 | [diff] [blame] | 34 | |
Alex Gaynor | 02fad00 | 2013-10-30 14:16:13 -0700 | [diff] [blame] | 35 | padder = padding.PKCS7(ciphers.AES.block_size).padder() |
| 36 | padded_data = padder.update(data) + padder.finalize() |
Alex Gaynor | de36e90 | 2013-10-31 10:10:44 -0700 | [diff] [blame] | 37 | encryptor = BlockCipher( |
| 38 | ciphers.AES(self.encryption_key), modes.CBC(iv) |
| 39 | ).encryptor() |
Alex Gaynor | 02fad00 | 2013-10-30 14:16:13 -0700 | [diff] [blame] | 40 | ciphertext = encryptor.update(padded_data) + encryptor.finalize() |
| 41 | |
| 42 | h = HMAC(self.signing_key, digestmod=hashes.SHA256) |
| 43 | h.update(b"\x80") |
| 44 | h.update(struct.pack(">Q", current_time)) |
| 45 | h.update(iv) |
| 46 | h.update(ciphertext) |
| 47 | hmac = h.digest() |
| 48 | return base64.urlsafe_b64encode( |
| 49 | b"\x80" + struct.pack(">Q", current_time) + iv + ciphertext + hmac |
| 50 | ) |
Alex Gaynor | bbeba71 | 2013-10-30 14:29:58 -0700 | [diff] [blame] | 51 | |
Alex Gaynor | 5e87dfd | 2013-10-31 09:46:03 -0700 | [diff] [blame] | 52 | def decrypt(self, data, ttl=None, current_time=None): |
Alex Gaynor | 38f3455 | 2013-10-31 14:50:00 -0700 | [diff] [blame] | 53 | if isinstance(data, six.text_type): |
Alex Gaynor | c1ea0a0 | 2013-10-31 15:03:53 -0700 | [diff] [blame^] | 54 | raise TypeError( |
| 55 | "Unicode-objects must be encoded before decryption" |
| 56 | ) |
Alex Gaynor | 38f3455 | 2013-10-31 14:50:00 -0700 | [diff] [blame] | 57 | |
Alex Gaynor | 5e87dfd | 2013-10-31 09:46:03 -0700 | [diff] [blame] | 58 | if current_time is None: |
| 59 | current_time = int(time.time()) |
Alex Gaynor | 38f3455 | 2013-10-31 14:50:00 -0700 | [diff] [blame] | 60 | |
| 61 | try: |
| 62 | data = base64.urlsafe_b64decode(data) |
| 63 | except TypeError: |
| 64 | raise InvalidToken |
| 65 | |
Alex Gaynor | 5c5342e | 2013-10-31 11:25:54 -0700 | [diff] [blame] | 66 | assert six.indexbytes(data, 0) == 0x80 |
Alex Gaynor | f593848 | 2013-10-30 14:34:55 -0700 | [diff] [blame] | 67 | timestamp = data[1:9] |
| 68 | iv = data[9:25] |
| 69 | ciphertext = data[25:-32] |
Alex Gaynor | bbeba71 | 2013-10-30 14:29:58 -0700 | [diff] [blame] | 70 | if ttl is not None: |
Alex Gaynor | 5e87dfd | 2013-10-31 09:46:03 -0700 | [diff] [blame] | 71 | if struct.unpack(">Q", timestamp)[0] + ttl < current_time: |
Alex Gaynor | 38f3455 | 2013-10-31 14:50:00 -0700 | [diff] [blame] | 72 | raise InvalidToken |
Alex Gaynor | bbeba71 | 2013-10-30 14:29:58 -0700 | [diff] [blame] | 73 | h = HMAC(self.signing_key, digestmod=hashes.SHA256) |
| 74 | h.update(data[:-32]) |
| 75 | hmac = h.digest() |
Alex Gaynor | 38f3455 | 2013-10-31 14:50:00 -0700 | [diff] [blame] | 76 | |
Alex Gaynor | bbeba71 | 2013-10-30 14:29:58 -0700 | [diff] [blame] | 77 | if not constant_time_compare(hmac, data[-32:]): |
Alex Gaynor | 38f3455 | 2013-10-31 14:50:00 -0700 | [diff] [blame] | 78 | raise InvalidToken |
| 79 | |
Alex Gaynor | de36e90 | 2013-10-31 10:10:44 -0700 | [diff] [blame] | 80 | decryptor = BlockCipher( |
| 81 | ciphers.AES(self.encryption_key), modes.CBC(iv) |
| 82 | ).decryptor() |
Alex Gaynor | 2b21b12 | 2013-10-31 09:39:25 -0700 | [diff] [blame] | 83 | plaintext_padded = decryptor.update(ciphertext) + decryptor.finalize() |
Alex Gaynor | bbeba71 | 2013-10-30 14:29:58 -0700 | [diff] [blame] | 84 | unpadder = padding.PKCS7(ciphers.AES.block_size).unpadder() |
Alex Gaynor | 38f3455 | 2013-10-31 14:50:00 -0700 | [diff] [blame] | 85 | |
| 86 | unpadded = unpadder.update(plaintext_padded) |
| 87 | try: |
| 88 | unpadded += unpadder.finalize() |
| 89 | except ValueError: |
| 90 | raise InvalidToken |
| 91 | return unpadded |
Alex Gaynor | bbeba71 | 2013-10-30 14:29:58 -0700 | [diff] [blame] | 92 | |
Alex Gaynor | de36e90 | 2013-10-31 10:10:44 -0700 | [diff] [blame] | 93 | |
Alex Gaynor | bbeba71 | 2013-10-30 14:29:58 -0700 | [diff] [blame] | 94 | def constant_time_compare(a, b): |
| 95 | # TOOD: replace with a cffi function |
| 96 | assert isinstance(a, bytes) and isinstance(b, bytes) |
| 97 | if len(a) != len(b): |
| 98 | return False |
| 99 | result = 0 |
Alex Gaynor | 139cf46 | 2013-10-31 11:36:01 -0700 | [diff] [blame] | 100 | for i in range(len(a)): |
Alex Gaynor | bbeba71 | 2013-10-30 14:29:58 -0700 | [diff] [blame] | 101 | result |= six.indexbytes(a, i) ^ six.indexbytes(b, i) |
| 102 | return result == 0 |