Skip to content

Commit 2c56dc9

Browse files
committed
Improve encryption
1 parent 03ae994 commit 2c56dc9

2 files changed

Lines changed: 15 additions & 90 deletions

File tree

Lines changed: 13 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,37 @@
11
"""encryption_helpers: functions for encrypting and decrypting data."""
22

3-
import base64
43
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
95

106
from app.settings import settings
117

128

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.
2611
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.
2715
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.
3018
3119
Args:
3220
----
33-
data (str): The data to be encrypted.
21+
token: The plaintext token (UUID string) to hash.
3422
3523
Returns:
3624
-------
37-
str: The Base64 encoded encrypted data including the IV.
25+
A 64-character lowercase hex string (256-bit HMAC digest).
3826
3927
"""
4028
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()
8530

8631

8732
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-
9233
import uuid
9334

94-
# Original plaintext data
9535
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)

app/services/users/user_handler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ async def create_pw_reset_token(
178178
query = str(uuid4())
179179
pw_reset_token = db_models.PasswordResetToken(
180180
user_id=user.id,
181-
encrypted_query=encryption_handler.encrypt(query),
181+
encrypted_query=encryption_handler.hash_token(query),
182182
created_timestamp=datetime.now(UTC),
183183
expires_timestamp=datetime.now(UTC) + timedelta(minutes=PW_RESET_TOKEN_EXPIRATION_MINUTES),
184184
)
@@ -190,7 +190,7 @@ async def create_pw_reset_token(
190190

191191
async def assert_token_is_valid(db: AsyncSession, query: str) -> db_models.PasswordResetToken:
192192
"""Get a password reset token by query."""
193-
encrypted_query = encryption_handler.encrypt(query)
193+
encrypted_query = encryption_handler.hash_token(query)
194194

195195
stmt = select(db_models.PasswordResetToken).where(
196196
db_models.PasswordResetToken.encrypted_query == encrypted_query

0 commit comments

Comments
 (0)