How API Keys Are Encrypted in FreeLLMAPI: AES-256-GCM Implementation

FreeLLMAPI encrypts all provider API keys using AES-256-GCM via the Node.js crypto module, storing the ciphertext, initialization vector, and authentication tag in SQLite while requiring a 64-character hex master key from the ENCRYPTION_KEY environment variable.

FreeLLMAPI is an open-source LLM API gateway that securely stores provider credentials using industry-standard encryption. Understanding how API keys are encrypted in FreeLLMAPI is essential for security audits and self-hosted deployments. The implementation isolates all cryptographic logic in server/src/lib/crypto.ts, ensuring that sensitive keys never persist in plaintext and are only decrypted transiently in memory when needed.

Encryption Architecture Overview

The encryption workflow centers on AES-256-GCM (Galois/Counter Mode), providing authenticated encryption with associated data (AEAD). Every API key undergoes the following transformation:

  1. PlaintextCiphertext + IV + Auth Tag
  2. Storage → SQLite api_keys table (encrypted_key, iv, auth_tag columns)
  3. Retrieval → Decryption using the master key cached at startup

The core implementation lives in server/src/lib/crypto.ts and is consumed by the key-management routes (server/src/routes/keys.ts) and service layers including the request router, media service, embeddings service, and health checks.

Initializing the Master Encryption Key

Before any encryption or decryption occurs, the system must initialize a 32-byte master key.

Production Configuration

In production, FreeLLMAPI expects a hex-encoded 64-character key in the ENCRYPTION_KEY environment variable. The initEncryptionKey() function validates this input using parseHexKey(), which guarantees exactly 32 bytes (64 hex characters) and aborts early if the format is incorrect.

// server/src/lib/crypto.ts
export function initEncryptionKey(db: Database.Database): void {
  const envKey = process.env.ENCRYPTION_KEY;
  if (envKey && envKey !== PLACEHOLDER_KEY) {
    cachedKey = parseHexKey(envKey, 'env');       // ← validate length + hex format
    return;
  }
  // dev fallback → read from DB or generate a new one

}

The code explicitly refuses to use placeholder values (such as your-64-char-hex-key-here) in production environments.

Development Fallback

When NODE_ENV !== 'production', the system auto-generates a cryptographically secure key, persists it in the local settings table, and reuses it for the lifetime of the database. This temporary key disappears when the database is reset, enforcing the requirement that production deployments must supply a real encryption key.

Encrypting New API Keys

When a user adds a new provider key via POST /keys or POST /keys/custom, the plaintext is passed to the encrypt() function. This function generates a random 16-byte initialization vector (IV) and produces a 16-byte authentication tag to prevent tampering.

// server/src/lib/crypto.ts
export function encrypt(text: string): { encrypted: string; iv: string; authTag: string } {
  const key = getEncryptionKey();                 // ← cached key from initEncryptionKey()
  const iv  = crypto.randomBytes(16);             // 96‑bit nonce
  const cipher = crypto.createCipheriv(ALGORITHM, key, iv);

  let encrypted = cipher.update(text, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  const authTag = cipher.getAuthTag().toString('hex');

  return { encrypted, iv: iv.toString('hex'), authTag };
}

Key parameters:

  • Algorithm: aes-256-gcm
  • IV: 16 random bytes (128 bits), stored as hex
  • Auth Tag: 16 bytes (AUTH_TAG_BYTES = 16), preventing truncation attacks

The resulting encrypted, iv, and authTag strings are inserted into the api_keys table. The plaintext never touches the database.

Decrypting Keys at Runtime

Whenever a request requires the real provider key—such as when routing to LLM providers, processing media, generating embeddings, or running health checks—the stored values are retrieved and fed to decrypt():

// server/src/lib/crypto.ts
export function decrypt(encrypted: string, iv: string, authTag: string): string {
  const key = getEncryptionKey();
  const decipher = crypto.createDecipheriv(
    ALGORITHM,
    key,
    Buffer.from(iv, 'hex'),
    { authTagLength: AUTH_TAG_BYTES }
  );
  decipher.setAuthTag(Buffer.from(authTag, 'hex'));

  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  return decrypted;
}

This routine is used throughout the codebase. For example, in server/src/routes/keys.ts, the system decrypts keys to display masked versions:

// server/src/routes/keys.ts
const realKey = decrypt(row.encrypted_key, row.iv, row.auth_tag);
maskedKey = maskKey(realKey);

If decryption fails due to corrupted data or a missing master key, the route falls back to "[decrypt failed]" rather than exposing error details.

Security Hardening Measures

FreeLLMAPI implements several defenses against common cryptographic attacks:

  • Key Validation: The parseHexKey utility enforces exactly 64 hex characters (32 bytes), preventing weak key configurations.
  • Placeholder Rejection: The initialization logic refuses to encrypt data using default placeholder strings, ensuring production keys are explicitly configured.
  • Auth Tag Integrity: By setting AUTH_TAG_BYTES = 16, the code forces a full-size GCM authentication tag, closing the 4-byte truncation attack vector.
  • Memory Safety: Only masked versions of keys (via maskKey()) are returned to clients; the raw plaintext exists only transiently in server memory during active requests.
  • Isolation: All encryption logic is confined to server/src/lib/crypto.ts, making the surface area auditable and simplifying future algorithm updates.

Complete Key Lifecycle Example

The following illustrates the end-to-end flow from storing a Google API key to using it in a request:

1. Store the key (client request):

curl -X POST http://localhost:3000/keys \
  -H "Content-Type: application/json" \
  -d '{"platform":"google","key":"sk-abcdef1234567890","label":"My Google key"}'

Internally, the server calls encrypt() and stores the resulting encrypted_key, iv, and auth_tag in SQLite.

2. Retrieve masked keys (list view):

curl http://localhost:3000/keys

The server decrypts each entry, applies maskKey() to show only the last four characters, and returns the masked view.

3. Use in internal routing (service layer):

import { getDb } from '../db/index.js';
import { decrypt } from '../lib/crypto.js';

function getProviderKey(platform: string) {
  const db = getDb();
  const row = db.prepare(`
    SELECT encrypted_key, iv, auth_tag
    FROM api_keys
    WHERE platform = ? AND enabled = 1
    ORDER BY created_at DESC LIMIT 1
  `).get(platform);
  return decrypt(row.encrypted_key, row.iv, row.auth_tag);
}

// Usage in server/src/services/router.ts (line 585)
const apiKey = getProviderKey('google');
// Pass apiKey to provider SDK...

Summary

  • Algorithm: FreeLLMAPI uses AES-256-GCM for authenticated encryption of all provider API keys.
  • Storage: Encrypted keys are stored in SQLite with separate columns for encrypted_key, iv, and auth_tag.
  • Master Key: Requires a 64-character hex ENCRYPTION_KEY environment variable in production; auto-generates temporary keys in development.
  • Implementation: Core logic resides in server/src/lib/crypto.ts, consumed by routes and services including server/src/routes/keys.ts and server/src/services/router.ts.
  • Security: Enforces full-length authentication tags, rejects placeholder keys, and only exposes masked key representations to clients.

Frequently Asked Questions

What encryption algorithm does FreeLLMAPI use?

FreeLLMAPI uses AES-256-GCM (Galois/Counter Mode) as implemented in the Node.js crypto module. This provides authenticated encryption, ensuring both confidentiality and integrity of the stored API keys.

How long does the encryption key need to be?

The master encryption key must be exactly 64 hexadecimal characters (representing 32 bytes). The parseHexKey() validation function in server/src/lib/crypto.ts enforces this length and format, rejecting shorter or non-hex keys.

Where are the encrypted keys stored?

Encrypted keys are stored in the SQLite api_keys table with three distinct columns: encrypted_key (the ciphertext), iv (the initialization vector), and auth_tag (the GCM authentication tag). The plaintext is never written to disk.

What happens if the ENCRYPTION_KEY is lost?

If the master key is lost or changed, previously encrypted API keys become permanently unreadable. FreeLLMAPI will return "[decrypt failed]" when attempting to decrypt affected keys. There is no key recovery mechanism; you must delete and re-add the provider keys with the new master key.

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 →