"""AES-256-GCM encryption for instance passwords.""" import base64 import os from cryptography.hazmat.primitives.ciphers.aead import AESGCM from app.core.config import get_settings _NONCE_SIZE = 12 # 96-bit nonce for AES-GCM def _get_key() -> bytes: """Derive a 32-byte AES key from settings.SECRET_KEY.""" settings = get_settings() # Use first 32 bytes of utf-8 encoded secret key (padded if needed) raw = settings.SECRET_KEY.encode("utf-8") if len(raw) >= 32: return raw[:32] return raw.ljust(32, b"\x00") def encrypt_password(plain: str) -> str: """Encrypt a password string, return base64-encoded ciphertext (nonce + ciphertext).""" key = _get_key() nonce = os.urandom(_NONCE_SIZE) aesgcm = AESGCM(key) ct = aesgcm.encrypt(nonce, plain.encode("utf-8"), None) return base64.b64encode(nonce + ct).decode("ascii") def decrypt_password(encrypted: str) -> str: """Decrypt a base64-encoded encrypted password back to plaintext. If the value is not valid base64 (e.g., legacy plaintext), return as-is. """ try: key = _get_key() raw = base64.b64decode(encrypted, validate=True) if len(raw) <= _NONCE_SIZE: return encrypted # too short to be encrypted nonce = raw[:_NONCE_SIZE] ct = raw[_NONCE_SIZE:] aesgcm = AESGCM(key) return aesgcm.decrypt(nonce, ct, None).decode("utf-8") except Exception: # Not encrypted (legacy plaintext password) — return as-is return encrypted