Protecting API Keys with AES-GCM and Runtime Injection
API keys and secrets must be protected throughout their lifecycle: generation, storage, transmission, and usage. This post describes a design using AES-GCM (Galois/Counter Mode) for authenticated encryption, encrypted storage, and runtime decryption to minimize secret exposure.
Encryption & Key Management
API keys are generated as 256-bit random values and encrypted using AES-256-GCM. The encryption key is derived from a master key stored in a hardware security module (HSM) or retrieved from an environment variable at deployment time. GCM provides both confidentiality and integrity check tags, ensuring that any tampering is detected.
Encrypted Storage
Encrypted keys are stored in a SQLite database with the schema:
CREATE TABLE api_keys ( id INTEGER PRIMARY KEY, encrypted_key BLOB NOT NULL, nonce BLOB NOT NULL, auth_tag BLOB NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );The nonce and authentication tag are stored alongside the ciphertext, as they are required for decryption and integrity verification.
Runtime Decryption
At runtime, the application retrieves the encrypted key and uses the same master key to decrypt it on-demand:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM def decrypt_key(encrypted_key, nonce, auth_tag, master_key): aad = b"" # Additional authenticated data aesgcm = AESGCM(master_key) decrypted = aesgcm.decrypt(nonce, encrypted_key + auth_tag, aad) return decrypted.decode()Decryption never exposes the master key to the application code path; it is used only within the AESGCM primitive.
Data-Flow Diagram
Key Generation → AES-GCM Encrypt → Store (nonce, ciphertext, auth_tag) → Runtime → Decrypt (master key from HSM) → Use in API calls → Securely erase from memoryNonce Handling
A unique 12-byte nonce must be generated for each encryption operation. Reusing a nonce with the same key compromises security. The implementation uses a cryptographically secure random generator:
import os nonce = os.urandom(12)Key Separation
Different API keys use separate master keys. This key isolation ensures that a compromise of one key does not expose all secrets. Key identifiers are stored in the database to enable rotation without affecting other keys.
Secure Logging
Logging must never output raw API keys. A log filter replaces key patterns with masked values:
import re LOG_PATTERN = re.compile(r'(?<=[=]).{0,16}(?=[&])') def mask_log(value): return LOG_PATTERN.sub('***MASKED***', value)Testing
Tests verify that:
- Encrypted keys can be decrypted correctly after storage and retrieval.
- Tampered ciphertexts are rejected (auth tag mismatch).
- Nonce reuse raises an exception.
- Log masking works for various key formats.