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

> Understand the AES-256-GCM encryption flow in FreeLLMAPI. Learn how this guide details the protection of API keys and proxy URLs at rest with a 16-byte IV and authentication tag.

- Repository: [Tashfeen/freellmapi](https://github.com/tashfeenahmed/freellmapi)
- Tags: deep-dive
- Published: 2026-08-30

---

**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`](https://github.com/tashfeenahmed/freellmapi/blob/main/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.

```ts
// 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` |

```ts
// 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.

```ts
// 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`](https://github.com/tashfeenahmed/freellmapi/blob/main/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`](https://github.com/tashfeenahmed/freellmapi/blob/main/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`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/key-proxy.ts) wraps the core crypto functions with proxy-specific logic, including silent fallback on decryption failure.

```ts
// 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

```ts
// 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`](https://github.com/tashfeenahmed/freellmapi/blob/main/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`](https://github.com/tashfeenahmed/freellmapi/blob/main/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.