|
1 | 1 | """encryption_helpers: functions for encrypting and decrypting data.""" |
2 | 2 |
|
3 | | -import base64 |
4 | 3 | import hashlib |
5 | | - |
6 | | -from cryptography.hazmat.backends import default_backend |
7 | | -from cryptography.hazmat.primitives import padding |
8 | | -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes |
| 4 | +import hmac |
9 | 5 |
|
10 | 6 | from app.settings import settings |
11 | 7 |
|
12 | 8 |
|
13 | | -def derive_iv(plaintext: str) -> bytes: |
14 | | - """Derive a consistent IV from the plaintext using a hash function. |
15 | | -
|
16 | | - Args: |
17 | | - ---- |
18 | | - plaintext (str): The plaintext from which to derive the IV. |
19 | | -
|
20 | | - Returns: |
21 | | - ------- |
22 | | - bytes: A 16-byte IV derived from the plaintext. |
23 | | -
|
24 | | - """ |
25 | | - return hashlib.sha256(plaintext.encode("utf-8")).digest()[:16] |
| 9 | +def hash_token(token: str) -> str: |
| 10 | + """Return an HMAC-SHA256 hex digest of `token` keyed with the encryption key. |
26 | 11 |
|
| 12 | + Using HMAC rather than a bare SHA-256 hash prevents offline rainbow-table |
| 13 | + attacks: an attacker who obtains the DB rows cannot pre-compute hashes |
| 14 | + without also knowing the secret key. |
27 | 15 |
|
28 | | -def encrypt(data: str) -> str: |
29 | | - """Encrypt the given data using AES encryption and encodes it in Base64. |
| 16 | + The token is a UUID (128 bits of entropy), so HMAC-SHA256 is sufficient — |
| 17 | + a slow KDF such as bcrypt is not required. |
30 | 18 |
|
31 | 19 | Args: |
32 | 20 | ---- |
33 | | - data (str): The data to be encrypted. |
| 21 | + token: The plaintext token (UUID string) to hash. |
34 | 22 |
|
35 | 23 | Returns: |
36 | 24 | ------- |
37 | | - str: The Base64 encoded encrypted data including the IV. |
| 25 | + A 64-character lowercase hex string (256-bit HMAC digest). |
38 | 26 |
|
39 | 27 | """ |
40 | 28 | key_bytes = bytes.fromhex(settings.encryption_key) |
41 | | - data_bytes = data.encode("utf-8") |
42 | | - iv = derive_iv(data) |
43 | | - cipher = Cipher(algorithms.AES(key_bytes), modes.CBC(iv), backend=default_backend()) |
44 | | - encryptor = cipher.encryptor() |
45 | | - |
46 | | - # Pad data to be a multiple of block size |
47 | | - padder = padding.PKCS7(algorithms.AES.block_size).padder() |
48 | | - padded_data = padder.update(data_bytes) + padder.finalize() |
49 | | - |
50 | | - encrypted_data = encryptor.update(padded_data) + encryptor.finalize() |
51 | | - encrypted_data_with_iv = iv + encrypted_data |
52 | | - |
53 | | - # Encode the encrypted data with IV in Base64 |
54 | | - return base64.b64encode(encrypted_data_with_iv).decode("utf-8") |
55 | | - |
56 | | - |
57 | | -def decrypt(encoded_data: str) -> str: |
58 | | - """Decrypt the given Base64 encoded encrypted data using AES decryption. |
59 | | -
|
60 | | - Args: |
61 | | - ---- |
62 | | - encoded_data (str): The Base64 encoded encrypted data including the IV. |
63 | | -
|
64 | | - Returns: |
65 | | - ------- |
66 | | - str: The decrypted data as a string. |
67 | | -
|
68 | | - """ |
69 | | - key_bytes = bytes.fromhex(settings.encryption_key) |
70 | | - encrypted_data_with_iv = base64.b64decode(encoded_data) |
71 | | - |
72 | | - iv = encrypted_data_with_iv[:16] |
73 | | - encrypted_data = encrypted_data_with_iv[16:] |
74 | | - |
75 | | - cipher = Cipher(algorithms.AES(key_bytes), modes.CBC(iv), backend=default_backend()) |
76 | | - decryptor = cipher.decryptor() |
77 | | - |
78 | | - padded_data = decryptor.update(encrypted_data) + decryptor.finalize() |
79 | | - |
80 | | - # Unpad data |
81 | | - unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() |
82 | | - data_bytes = unpadder.update(padded_data) + unpadder.finalize() |
83 | | - |
84 | | - return data_bytes.decode("utf-8") |
| 29 | + return hmac.new(key_bytes, token.encode("utf-8"), hashlib.sha256).hexdigest() |
85 | 30 |
|
86 | 31 |
|
87 | 32 | if __name__ == "__main__": # pragma: no cover |
88 | | - # Encryption key as a 64-character hex string (256 bits / 32 bytes) |
89 | | - # key = os.urandom(32).hex() # noqa: ERA001 (commented-out code) |
90 | | - print(f"Key (hex): {settings.encryption_key}") # noqa: T201 (print used for example) |
91 | | - |
92 | 33 | import uuid |
93 | 34 |
|
94 | | - # Original plaintext data |
95 | 35 | data = str(uuid.uuid4()) |
96 | | - print("Original Data:", data) # noqa: T201 (print used for example) |
97 | | - |
98 | | - # Encrypt the data to get the encrypted value |
99 | | - encrypted_data = encrypt(data) |
100 | | - print("Encrypted Data:", encrypted_data) # noqa: T201 (print used for example) |
101 | | - |
102 | | - # Decrypt the data to get the original value back |
103 | | - decrypted_data = decrypt(encrypted_data) |
104 | | - print("Decrypted Data:", decrypted_data) # noqa: T201 (print used for example) |
105 | | - |
106 | | - # To search for the encrypted data if you know the decrypted data |
107 | | - known_decrypted_data = data |
108 | | - encrypted_search_value = encrypt(known_decrypted_data) |
109 | | - print("Encrypted Search Value:", encrypted_search_value) # noqa: T201 (print used for example) |
110 | | - |
111 | | - # Check if the encrypted data matches the search value |
112 | | - print("Match found:", encrypted_data == encrypted_search_value) # noqa: T201 (print used for example) |
| 36 | + print("Token:", data) # noqa: T201 (print used for example) |
| 37 | + print("Hashed:", hash_token(data)) # noqa: T201 (print used for example) |
0 commit comments