refactor(security): improve encryption using PyCryptodome and PBKDF2

Replace the `cryptography` library with `pycryptodome` for password encryption.

The previous implementation used AES-GCM with a static key derived from a
hardcoded secret. This change introduces a more robust security model by:
- Using PBKDF2 to derive the encryption key from the secret.
- Adding a unique, randomly generated salt for each encrypted password.

This significantly enhances security by protecting against rainbow table
and pre-computation attacks.

BREAKING CHANGE: The password encryption format has changed. All previously
encrypted passwords stored in the database are now invalid and will need
to be reset.
This commit is contained in:
Karl 2025-07-14 11:55:13 +01:00
parent 79a2f6e944
commit aa1b9d7281

View File

@ -1,30 +1,26 @@
import os import os
import hashlib from Crypto.Cipher import AES
from cryptography.hazmat.primitives.ciphers.aead import AESGCM from Crypto.Protocol.KDF import PBKDF2
from Crypto.Random import get_random_bytes
SECRET = "BBLBTV-DNS-PASSWORDS" SECRET = "BBLBTV-DNS-PASSWORDS"
KEY = hashlib.sha256(SECRET.encode()).digest() SALT_SIZE = 16
ALGORITHM = "aes-256-gcm" KEY_SIZE = 32
IV_LENGTH = 16 ITERATIONS = 100000
AUTH_TAG_LENGTH = 16
def encrypt_password(clear_string): def encrypt_password(clear_string):
iv = os.urandom(IV_LENGTH) salt = get_random_bytes(SALT_SIZE)
aesgcm = AESGCM(KEY) key = PBKDF2(SECRET, salt, dkLen=KEY_SIZE, count=ITERATIONS)
cipher = AES.new(key, AES.MODE_GCM)
ciphertext_and_tag = aesgcm.encrypt(iv, clear_string.encode(), None) ciphertext, tag = cipher.encrypt_and_digest(clear_string.encode())
ciphertext = ciphertext_and_tag[:-AUTH_TAG_LENGTH] return (salt + cipher.nonce + tag + ciphertext).hex()
tag = ciphertext_and_tag[-AUTH_TAG_LENGTH:]
return (iv + tag + ciphertext).hex()
def decrypt_password(encrypted_string): def decrypt_password(encrypted_string):
data = bytes.fromhex(encrypted_string) data = bytes.fromhex(encrypted_string)
salt = data[:SALT_SIZE]
iv = data[:IV_LENGTH] nonce = data[SALT_SIZE:SALT_SIZE + 16]
tag = data[IV_LENGTH:IV_LENGTH + AUTH_TAG_LENGTH] tag = data[SALT_SIZE + 16:SALT_SIZE + 32]
ciphertext = data[IV_LENGTH + AUTH_TAG_LENGTH:] ciphertext = data[SALT_SIZE + 32:]
key = PBKDF2(SECRET, salt, dkLen=KEY_SIZE, count=ITERATIONS)
aesgcm = AESGCM(KEY) cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
decrypted_bytes = aesgcm.decrypt(iv, ciphertext + tag, None) return cipher.decrypt_and_verify(ciphertext, tag).decode()
return decrypted_bytes.decode()