59 lines
2.0 KiB
Python
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") |