Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/cryptography/fernet.py: 55%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is dual licensed under the terms of the Apache License, Version
2# 2.0, and the BSD License. See the LICENSE file in the root of this repository
3# for complete details.
5from __future__ import annotations
7import base64
8import binascii
9import os
10import time
11import typing
12from collections.abc import Iterable
14from cryptography import utils
15from cryptography.exceptions import InvalidSignature
16from cryptography.hazmat.primitives import hashes, padding
17from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
18from cryptography.hazmat.primitives.hmac import HMAC
21class InvalidToken(Exception):
22 pass
25_MAX_CLOCK_SKEW = 60
27# Hoisted to module level so each operation doesn't reconstruct them.
28_PKCS7_128 = padding.PKCS7(128)
29_SHA256 = hashes.SHA256()
32class Fernet:
33 def __init__(
34 self,
35 key: bytes | str,
36 backend: typing.Any = None,
37 ) -> None:
38 try:
39 key = base64.urlsafe_b64decode(key)
40 except binascii.Error as exc:
41 raise ValueError(
42 "Fernet key must be 32 url-safe base64-encoded bytes."
43 ) from exc
44 if len(key) != 32:
45 raise ValueError(
46 "Fernet key must be 32 url-safe base64-encoded bytes."
47 )
49 self._signing_key = key[:16]
50 self._encryption_key = key[16:]
51 self._aes = algorithms.AES(self._encryption_key)
53 @classmethod
54 def generate_key(cls) -> bytes:
55 return base64.urlsafe_b64encode(os.urandom(32))
57 def encrypt(self, data: bytes) -> bytes:
58 return self.encrypt_at_time(data, int(time.time()))
60 def encrypt_at_time(self, data: bytes, current_time: int) -> bytes:
61 iv = os.urandom(16)
62 return self._encrypt_from_parts(data, current_time, iv)
64 def _encrypt_from_parts(
65 self, data: bytes, current_time: int, iv: bytes
66 ) -> bytes:
67 utils._check_bytes("data", data)
69 padder = _PKCS7_128.padder()
70 padded_data = padder.update(data) + padder.finalize()
71 encryptor = Cipher(self._aes, modes.CBC(iv)).encryptor()
72 ciphertext = encryptor.update(padded_data) + encryptor.finalize()
74 basic_parts = (
75 b"\x80"
76 + current_time.to_bytes(length=8, byteorder="big")
77 + iv
78 + ciphertext
79 )
81 h = HMAC(self._signing_key, _SHA256)
82 h.update(basic_parts)
83 hmac = h.finalize()
84 return base64.urlsafe_b64encode(basic_parts + hmac)
86 def decrypt(self, token: bytes | str, ttl: int | None = None) -> bytes:
87 timestamp, data = Fernet._get_unverified_token_data(token)
88 if ttl is None:
89 time_info = None
90 else:
91 time_info = (ttl, int(time.time()))
92 return self._decrypt_data(data, timestamp, time_info)
94 def decrypt_at_time(
95 self, token: bytes | str, ttl: int, current_time: int
96 ) -> bytes:
97 if ttl is None:
98 raise ValueError(
99 "decrypt_at_time() can only be used with a non-None ttl"
100 )
101 timestamp, data = Fernet._get_unverified_token_data(token)
102 return self._decrypt_data(data, timestamp, (ttl, current_time))
104 def extract_timestamp(self, token: bytes | str) -> int:
105 timestamp, data = Fernet._get_unverified_token_data(token)
106 # Verify the token was not tampered with.
107 self._verify_signature(data)
108 return timestamp
110 @staticmethod
111 def _get_unverified_token_data(token: bytes | str) -> tuple[int, bytes]:
112 if not isinstance(token, (str, bytes)):
113 raise TypeError("token must be bytes or str")
115 try:
116 data = base64.urlsafe_b64decode(token)
117 except (TypeError, binascii.Error):
118 raise InvalidToken
120 if not data or data[0] != 0x80:
121 raise InvalidToken
123 if len(data) < 9:
124 raise InvalidToken
126 timestamp = int.from_bytes(data[1:9], byteorder="big")
127 return timestamp, data
129 def _verify_signature(self, data: bytes) -> None:
130 h = HMAC(self._signing_key, _SHA256)
131 h.update(memoryview(data)[:-32])
132 try:
133 h.verify(data[-32:])
134 except InvalidSignature:
135 raise InvalidToken
137 def _decrypt_data(
138 self,
139 data: bytes,
140 timestamp: int,
141 time_info: tuple[int, int] | None,
142 ) -> bytes:
143 if time_info is not None:
144 ttl, current_time = time_info
145 if timestamp + ttl < current_time:
146 raise InvalidToken
148 if current_time + _MAX_CLOCK_SKEW < timestamp:
149 raise InvalidToken
151 self._verify_signature(data)
153 mv = memoryview(data)
154 iv = data[9:25]
155 ciphertext = mv[25:-32]
156 decryptor = Cipher(self._aes, modes.CBC(iv)).decryptor()
157 plaintext_padded = decryptor.update(ciphertext)
158 try:
159 plaintext_padded += decryptor.finalize()
160 except ValueError:
161 raise InvalidToken
162 unpadder = _PKCS7_128.unpadder()
164 unpadded = unpadder.update(plaintext_padded)
165 try:
166 unpadded += unpadder.finalize()
167 except ValueError:
168 raise InvalidToken
169 return unpadded
172class MultiFernet:
173 def __init__(self, fernets: Iterable[Fernet]):
174 fernets = list(fernets)
175 if not fernets:
176 raise ValueError(
177 "MultiFernet requires at least one Fernet instance"
178 )
179 self._fernets = fernets
181 def encrypt(self, msg: bytes) -> bytes:
182 return self.encrypt_at_time(msg, int(time.time()))
184 def encrypt_at_time(self, msg: bytes, current_time: int) -> bytes:
185 return self._fernets[0].encrypt_at_time(msg, current_time)
187 def rotate(self, msg: bytes | str) -> bytes:
188 timestamp, data = Fernet._get_unverified_token_data(msg)
189 for f in self._fernets:
190 try:
191 p = f._decrypt_data(data, timestamp, None)
192 break
193 except InvalidToken:
194 pass
195 else:
196 raise InvalidToken
198 iv = os.urandom(16)
199 return self._fernets[0]._encrypt_from_parts(p, timestamp, iv)
201 def decrypt(self, msg: bytes | str, ttl: int | None = None) -> bytes:
202 if ttl is None:
203 time_info = None
204 else:
205 time_info = (ttl, int(time.time()))
206 # Parse the token once rather than once per key.
207 timestamp, data = Fernet._get_unverified_token_data(msg)
208 for f in self._fernets:
209 try:
210 return f._decrypt_data(data, timestamp, time_info)
211 except InvalidToken:
212 pass
213 raise InvalidToken
215 def decrypt_at_time(
216 self, msg: bytes | str, ttl: int, current_time: int
217 ) -> bytes:
218 if ttl is None:
219 raise ValueError(
220 "decrypt_at_time() can only be used with a non-None ttl"
221 )
222 # Parse the token once rather than once per key.
223 timestamp, data = Fernet._get_unverified_token_data(msg)
224 for f in self._fernets:
225 try:
226 return f._decrypt_data(data, timestamp, (ttl, current_time))
227 except InvalidToken:
228 pass
229 raise InvalidToken
231 def extract_timestamp(self, msg: bytes | str) -> int:
232 # Parse the token once rather than once per key.
233 timestamp, data = Fernet._get_unverified_token_data(msg)
234 for f in self._fernets:
235 try:
236 f._verify_signature(data)
237 return timestamp
238 except InvalidToken:
239 pass
240 raise InvalidToken