AES-256-GCM Encryption Flow in FreeLLMAPI: Complete Technical Guide

FreeLLMAPI uses AES-256-GCM authenticated encryption with a 16-byte IV and authentication tag to protect all provider API keys and per-key proxy URLs at rest.

Locking down API credentials is critical for any LLM proxy server. In FreeLLMAPI, every sensitive value—OpenAI keys, Anthropic tokens, and SOCKS5 proxy URLs—gets encrypted before hitting the SQLite database. The implementation centers on Node.js's crypto module with strict GCM parameters enforced throughout the codebase.

Key Bootstrap and Initialization

The encryption lifecycle starts in server/src/lib/crypto.ts. The initEncryptionKey() function loads or generates the 256-bit master key when the server boots.

Key Resolution Priority

FreeLLMAPI checks for the encryption key in this strict order:

  1. ENCRYPTION_KEY environment variable – A 64-character hex string representing 32 bytes.
  2. .encryption-key file – Located next to the SQLite database, used in non-production environments when the env var is unset.
  3. Legacy settings table migration – Older installations have keys moved automatically to the key file.
  4. Fresh generation – In development, a random key is created and persisted if none exists.

The resolved key is cached in-memory (cachedKey) and reused for all encrypt/decrypt operations until process restart.

// server/src/lib/crypto.ts
import { initEncryptionKey } from './lib/crypto.js';

// Run once after database initialization
initEncryptionKey(db);   // Pulls ENCRYPTION_KEY or generates dev key

Encrypting Secrets: The AES-256-GCM Flow

The encrypt(text) function implements standard AES-256-GCM with these hardcoded parameters:

Parameter Value Constant
Algorithm aes-256-gcm —
Key length 32 bytes (256 bits) KEY_BYTES = 32
IV length 16 bytes IV_BYTES = 16
Auth tag length 16 bytes AUTH_TAG_BYTES = 16
// Encrypting a provider API key
import { encrypt } from './lib/crypto.js';

const apiKey = 'sk-abc123...';
const { encrypted, iv, authTag } = encrypt(apiKey);

// Store these three hex strings in the database:
// - encrypted: ciphertext
// - iv: initialization vector
// - authTag: GCM authentication tag

The function generates a fresh 16-byte IV per encryption, feeds it to crypto.createCipheriv(), streams the plaintext, and extracts the authentication tag via cipher.getAuthTag(). All outputs are hex-encoded for safe SQLite storage.

Decrypting Secrets with Authentication Verification

Decryption enforces tag verification before releasing plaintext. The decrypt(encrypted, iv, authTag) function reconstructs the cipher, injects the expected authentication tag, and rejects tampered ciphertext.

// Decrypting an API key for request handling
import { decrypt } from './lib/crypto.js';

const stored = row;   // {key_encrypted, key_iv, key_auth_tag}
const clearKey = decrypt(
  stored.key_encrypted!,
  stored.key_iv!,
  stored.key_auth_tag!
);

If authentication fails—indicating corruption or a key mismatch—the function throws. Callers in server/src/lib/key-proxy.ts catch these errors and fall back to safe defaults rather than crashing.

Database Schema for Encrypted Values

FreeLLMAPI stores encrypted data in split columns to preserve the IV and tag separately from ciphertext.

API Keys Table Structure

Column Purpose
key_encrypted Hex-encoded ciphertext
key_iv 16-byte IV as hex
key_auth_tag 16-byte GCM tag as hex

Per-Key Proxy Overrides

Migration 20260810_000001_api_key_proxy.ts added parallel columns for proxy URLs:

Column Purpose
proxy_encrypted Hex-encoded proxy URL ciphertext
proxy_iv IV for proxy encryption
proxy_auth_tag GCM tag for proxy encryption

When all three proxy columns are NULL, the override is treated as absent and the global proxy configuration applies.

Key-Proxy Helper Functions

server/src/lib/key-proxy.ts wraps the core crypto functions with proxy-specific logic, including silent fallback on decryption failure.

// Encrypt a per-key proxy URL
import { encryptProxyUrl } from './lib/key-proxy.js';

const proxy = 'socks5://alice:hunter2@proxy.example:1080';
const enc = encryptProxyUrl(proxy);
// Returns {encrypted, iv, authTag} ready for the three proxy_* columns

// Decrypt with fallback
import { decryptProxyUrl } from './lib/key-proxy.js';

const proxyUrl = decryptProxyUrl(row);   // Returns '' on any error

This fallback mechanism allows database portability—moving a DB to a server with a different ENCRYPTION_KEY degrades gracefully to global proxy settings rather than hard-failing requests.

Security Hardening Measures

The AES-256-GCM implementation includes multiple safety layers:

  • Fixed 16-byte authentication tags – The AUTH_TAG_BYTES = 16 constant prevents short-tag attacks that could forge valid ciphertext.
  • Atomic key file writes – New keys are written to temporary files and renamed, preventing corruption on crash.
  • Restricted file permissions – restrictToOwner() limits key file access to the running user only.
  • Key fingerprint logging – A truncated SHA-256 hash (sha256:<16-char>) allows key rotation verification without exposing secrets.

Complete Working Example

// server/src/index.ts or initialization script
import Database from 'better-sqlite3';
import { initEncryptionKey, encrypt, decrypt } from './lib/crypto.js';
import { encryptProxyUrl, decryptProxyUrl } from './lib/key-proxy.js';

const db = new Database('freellmapi.db');
initEncryptionKey(db);

// Storing a new API key with custom proxy
const providerKey = process.env.OPENAI_API_KEY!;
const { encrypted, iv, authTag } = encrypt(providerKey);

const proxy = 'http://corporate-proxy.internal:8080';
const proxyEnc = encryptProxyUrl(proxy);

// Insert into api_keys table
db.prepare(`
  INSERT INTO api_keys 
    (name, key_encrypted, key_iv, key_auth_tag, 
     proxy_encrypted, proxy_iv, proxy_auth_tag)
  VALUES (?, ?, ?, ?, ?, ?, ?)
`).run('production-key', encrypted, iv, authTag, 
       proxyEnc.encrypted, proxyEnc.iv, proxyEnc.authTag);

// Retrieving and using the key later
const row = db.prepare('SELECT * FROM api_keys WHERE name = ?')
  .get('production-key');

const decryptedKey = decrypt(row.key_encrypted, row.key_iv, row.key_auth_tag);
const resolvedProxy = decryptProxyUrl(row) || process.env.GLOBAL_PROXY_URL;

Summary

  • AES-256-GCM protects all secrets at rest with 256-bit keys, 16-byte IVs, and 16-byte authentication tags.
  • Key bootstrap in server/src/lib/crypto.ts resolves from ENCRYPTION_KEY env var, .encryption-key file, or generates fresh in development.
  • Split-column storage in api_keys preserves ciphertext, IV, and tag separately for API keys and proxy URLs.
  • Graceful degradation in server/src/lib/key-proxy.ts falls back to global proxy settings when decryption fails.
  • Hardcoded constants (KEY_BYTES, IV_BYTES, AUTH_TAG_BYTES) eliminate parameter confusion and short-tag vulnerabilities.

Frequently Asked Questions

What happens if the ENCRYPTION_KEY environment variable changes?

Existing encrypted data becomes unreadable. Decryption attempts will throw authentication errors, and per-key proxy lookups will fall back to global proxy settings. You must re-encrypt all secrets with the new key or restore the original key from backup.

Why does FreeLLMAPI use AES-256-GCM instead of AES-256-CBC?

GCM provides authenticated encryption: it simultaneously guarantees confidentiality and detects ciphertext tampering. CBC mode lacks built-in authentication and requires separate HMAC implementation, which is error-prone. The 16-byte GCM tag in FreeLLMAPI replaces that complexity with a single, well-audited construction.

How is the encryption key protected in production?

Production deployments must set ENCRYPTION_KEY as an environment variable. The .encryption-key file fallback is disabled in production mode. The key never persists to the database or logs—only a truncated SHA-256 fingerprint appears in diagnostic output.

Can I rotate the encryption key without downtime?

No seamless rotation mechanism exists in the current codebase. A key change requires: (1) decrypting all secrets with the old key, (2) re-encrypting with the new key, and (3) atomically updating all database rows. This operation should occur during a maintenance window with the server stopped.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →