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 memory

Nonce 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.
A.M. Rinas

Written by A.M. Rinas

Full-Stack Software Engineer working on web applications, DevOps automation, and databases. More about me on the about page, or say hi through the contact page and LinkedIn.

← Back to all articles

A.M. Rinas

Written by A.M. Rinas

Full-Stack Software Engineer working on web applications, DevOps automation, and databases. More about me on the about page, or say hi through the contact page and LinkedIn.

← Back to all articles