"""Key encryption at rest (spec sections 43-44). Private keys that the platform must keep (CA and server keys) are stored in PostgreSQL ONLY in encrypted form, using AES-256-GCM authenticated encryption. The master key comes from the SECRET_ENCRYPTION_KEY environment variable (Docker secret in production) and never touches the database. Wire format: "v1::" — the version prefix keeps the format evolvable without breaking existing rows. """ from __future__ import annotations import os from cryptography.hazmat.primitives.ciphers.aead import AESGCM from app.config import get_settings _FORMAT = "v1" def _master_key() -> bytes: """Derive the 32-byte AES key from the configured secret. The secret is hashed to exactly 32 bytes (AES-256) regardless of the length the operator chose for SECRET_ENCRYPTION_KEY. """ import hashlib secret = get_settings().secret_encryption_key.encode("utf-8") return hashlib.sha256(secret).digest() def encrypt_secret(plaintext: str) -> str: """Encrypt a PEM string into the storage format.""" nonce = os.urandom(12) # 96-bit nonce, GCM standard aesgcm = AESGCM(_master_key()) ciphertext = aesgcm.encrypt(nonce, plaintext.encode("utf-8"), None) return f"{_FORMAT}:{nonce.hex()}:{ciphertext.hex()}" def decrypt_secret(blob: str) -> str: """Decrypt a stored secret back to plaintext PEM.""" try: version, nonce_hex, ciphertext_hex = blob.split(":", 2) except ValueError as exc: raise ValueError("corrupt secret blob: bad format") from exc if version != _FORMAT: raise ValueError(f"unsupported secret blob version: {version}") aesgcm = AESGCM(_master_key()) try: plaintext = aesgcm.decrypt( bytes.fromhex(nonce_hex), bytes.fromhex(ciphertext_hex), None ) except Exception as exc: raise ValueError("secret decryption failed (wrong master key?)") from exc return plaintext.decode("utf-8")