backend redesign
This commit is contained in:
144
backend.old/src/secrets_manager/crypto.py
Normal file
144
backend.old/src/secrets_manager/crypto.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user