blob: ef64b7e936c12bb799a79ffd7b95b8c19ae7e33a [file] [log] [blame]
Alex Gaynor02fad002013-10-30 14:16:13 -07001import base64
2import os
3import struct
4import time
5
Alex Gaynorbbeba712013-10-30 14:29:58 -07006import six
7
Alex Gaynor02fad002013-10-30 14:16:13 -07008from cryptography.hazmat.primitives import padding, hashes
9from cryptography.hazmat.primitives.hmac import HMAC
10from cryptography.hazmat.primitives.block import BlockCipher, ciphers, modes
11
12
Alex Gaynor38f34552013-10-31 14:50:00 -070013class InvalidToken(Exception):
14 pass
15
16
Alex Gaynor02fad002013-10-30 14:16:13 -070017class Fernet(object):
18 def __init__(self, key):
19 super(Fernet, self).__init__()
Alex Gaynorcd47c4a2013-10-31 09:46:27 -070020 assert len(key) == 32
Alex Gaynor02fad002013-10-30 14:16:13 -070021 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 Gaynor38f34552013-10-31 14:50:00 -070030 if isinstance(data, six.text_type):
Alex Gaynorc1ea0a02013-10-31 15:03:53 -070031 raise TypeError(
32 "Unicode-objects must be encoded before encryption"
33 )
Alex Gaynor38f34552013-10-31 14:50:00 -070034
Alex Gaynor02fad002013-10-30 14:16:13 -070035 padder = padding.PKCS7(ciphers.AES.block_size).padder()
36 padded_data = padder.update(data) + padder.finalize()
Alex Gaynorde36e902013-10-31 10:10:44 -070037 encryptor = BlockCipher(
38 ciphers.AES(self.encryption_key), modes.CBC(iv)
39 ).encryptor()
Alex Gaynor02fad002013-10-30 14:16:13 -070040 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 Gaynorbbeba712013-10-30 14:29:58 -070051
Alex Gaynor5e87dfd2013-10-31 09:46:03 -070052 def decrypt(self, data, ttl=None, current_time=None):
Alex Gaynor38f34552013-10-31 14:50:00 -070053 if isinstance(data, six.text_type):
Alex Gaynorc1ea0a02013-10-31 15:03:53 -070054 raise TypeError(
55 "Unicode-objects must be encoded before decryption"
56 )
Alex Gaynor38f34552013-10-31 14:50:00 -070057
Alex Gaynor5e87dfd2013-10-31 09:46:03 -070058 if current_time is None:
59 current_time = int(time.time())
Alex Gaynor38f34552013-10-31 14:50:00 -070060
61 try:
62 data = base64.urlsafe_b64decode(data)
63 except TypeError:
64 raise InvalidToken
65
Alex Gaynor5c5342e2013-10-31 11:25:54 -070066 assert six.indexbytes(data, 0) == 0x80
Alex Gaynorf5938482013-10-30 14:34:55 -070067 timestamp = data[1:9]
68 iv = data[9:25]
69 ciphertext = data[25:-32]
Alex Gaynorbbeba712013-10-30 14:29:58 -070070 if ttl is not None:
Alex Gaynor5e87dfd2013-10-31 09:46:03 -070071 if struct.unpack(">Q", timestamp)[0] + ttl < current_time:
Alex Gaynor38f34552013-10-31 14:50:00 -070072 raise InvalidToken
Alex Gaynorbbeba712013-10-30 14:29:58 -070073 h = HMAC(self.signing_key, digestmod=hashes.SHA256)
74 h.update(data[:-32])
75 hmac = h.digest()
Alex Gaynor38f34552013-10-31 14:50:00 -070076
Alex Gaynorbbeba712013-10-30 14:29:58 -070077 if not constant_time_compare(hmac, data[-32:]):
Alex Gaynor38f34552013-10-31 14:50:00 -070078 raise InvalidToken
79
Alex Gaynorde36e902013-10-31 10:10:44 -070080 decryptor = BlockCipher(
81 ciphers.AES(self.encryption_key), modes.CBC(iv)
82 ).decryptor()
Alex Gaynor2b21b122013-10-31 09:39:25 -070083 plaintext_padded = decryptor.update(ciphertext) + decryptor.finalize()
Alex Gaynorbbeba712013-10-30 14:29:58 -070084 unpadder = padding.PKCS7(ciphers.AES.block_size).unpadder()
Alex Gaynor38f34552013-10-31 14:50:00 -070085
86 unpadded = unpadder.update(plaintext_padded)
87 try:
88 unpadded += unpadder.finalize()
89 except ValueError:
90 raise InvalidToken
91 return unpadded
Alex Gaynorbbeba712013-10-30 14:29:58 -070092
Alex Gaynorde36e902013-10-31 10:10:44 -070093
Alex Gaynorbbeba712013-10-30 14:29:58 -070094def 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 Gaynor139cf462013-10-31 11:36:01 -0700100 for i in range(len(a)):
Alex Gaynorbbeba712013-10-30 14:29:58 -0700101 result |= six.indexbytes(a, i) ^ six.indexbytes(b, i)
102 return result == 0