Files
ai/backend.old/src/secrets_manager/crypto.py
2026-03-11 18:47:11 -04:00

145 lines
4.0 KiB
Python

"""
Cryptographic utilities for secrets management.
Uses Argon2id for password-based key derivation and Fernet for encryption.
"""
import os
import secrets as secrets_module
from typing import Tuple
from argon2 import PasswordHasher
from argon2.low_level import hash_secret_raw, Type
from cryptography.fernet import Fernet
import base64
# Argon2id parameters (OWASP recommended for password-based KDF)
# These provide strong defense against GPU/ASIC attacks
ARGON2_TIME_COST = 3 # iterations
ARGON2_MEMORY_COST = 65536 # 64 MB
ARGON2_PARALLELISM = 4 # threads
ARGON2_HASH_LENGTH = 32 # bytes (256 bits for Fernet key)
ARGON2_SALT_LENGTH = 16 # bytes (128 bits)
def generate_salt() -> bytes:
"""Generate a cryptographically secure random salt."""
return secrets_module.token_bytes(ARGON2_SALT_LENGTH)
def derive_key_from_password(password: str, salt: bytes) -> bytes:
"""
Derive an encryption key from a password using Argon2id.
Args:
password: The master password
salt: The salt (must be consistent for the same password to work)
Returns:
32-byte key suitable for Fernet encryption
"""
password_bytes = password.encode('utf-8')
# Use Argon2id (hybrid mode - best of Argon2i and Argon2d)
raw_hash = hash_secret_raw(
secret=password_bytes,
salt=salt,
time_cost=ARGON2_TIME_COST,
memory_cost=ARGON2_MEMORY_COST,
parallelism=ARGON2_PARALLELISM,
hash_len=ARGON2_HASH_LENGTH,
type=Type.ID # Argon2id
)
return raw_hash
def create_fernet(key: bytes) -> Fernet:
"""
Create a Fernet cipher instance from a raw key.
Args:
key: 32-byte raw key from Argon2id
Returns:
Fernet instance for encryption/decryption
"""
# Fernet requires a URL-safe base64-encoded 32-byte key
fernet_key = base64.urlsafe_b64encode(key)
return Fernet(fernet_key)
def encrypt_data(data: bytes, key: bytes) -> bytes:
"""
Encrypt data using Fernet (AES-256-CBC).
Args:
data: Raw bytes to encrypt
key: 32-byte encryption key
Returns:
Encrypted data (includes IV and auth tag)
"""
fernet = create_fernet(key)
return fernet.encrypt(data)
def decrypt_data(encrypted_data: bytes, key: bytes) -> bytes:
"""
Decrypt data using Fernet.
Args:
encrypted_data: Encrypted bytes from encrypt_data
key: 32-byte encryption key (must match encryption key)
Returns:
Decrypted raw bytes
Raises:
cryptography.fernet.InvalidToken: If decryption fails (wrong key/corrupted data)
"""
fernet = create_fernet(key)
return fernet.decrypt(encrypted_data)
def create_verification_hash(password: str, salt: bytes) -> str:
"""
Create a verification hash to check if a password is correct.
This is NOT for storing the password - it's for verifying the password
unlocks the correct key without trying to decrypt the entire secrets file.
Args:
password: The master password
salt: The salt used for key derivation
Returns:
Base64-encoded hash for verification
"""
# Derive key and hash it again for verification
key = derive_key_from_password(password, salt)
# Simple hash of the key for verification (not security critical since
# the key itself is already derived from Argon2id)
verification = base64.b64encode(key[:16]).decode('ascii')
return verification
def verify_password(password: str, salt: bytes, verification_hash: str) -> bool:
"""
Verify a password against a verification hash.
Args:
password: Password to verify
salt: Salt used for key derivation
verification_hash: Expected verification hash
Returns:
True if password is correct, False otherwise
"""
computed_hash = create_verification_hash(password, salt)
# Constant-time comparison to prevent timing attacks
return secrets_module.compare_digest(computed_hash, verification_hash)