Files
mqtt-broker-stack/backend/app/crypto.py
T

59 lines
2.0 KiB
Python

"""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:<nonce-hex>:<ciphertext-hex>" — 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")