How FreeLLMAPI Encrypts API Keys at Rest with AES-256-GCM and Decrypts Them for Requests

FreeLLMAPI encrypts provider API keys using AES-256-GCM with a 32-byte master secret, storing the ciphertext, initialization vector, and authentication tag in SQLite, then decrypts them on-demand for outbound requests using the same master key.

FreeLLMAPI is an open-source API gateway that secures LLM provider credentials by never storing raw API keys in plaintext. According to the source code in tashfeenahmed/freellmapi, the application implements a robust encryption layer that ensures confidentiality at rest while maintaining integrity through Galois/Counter Mode authentication tags. This design guarantees that sensitive keys remain cryptographically protected even if the underlying database is compromised.

Master Encryption Key Initialization

The encryption subsystem centers on a single 32-byte master key that must be available before any cryptographic operations occur. In server/src/lib/crypto.ts, the initEncryptionKey() function handles secure key initialization at server startup.

Production Configuration via ENCRYPTION_KEY

In production environments, the system expects a 64-character hexadecimal string (representing 32 bytes) in the ENCRYPTION_KEY environment variable. The code validates this format strictly; if the variable is missing or malformed, the server will fail to start, preventing accidental operation without encryption.

Development Mode Key Generation

When process.env.NODE_ENV !== 'production', FreeLLMAPI falls back to automatic key generation for local development. The logic:

  1. Checks for an existing key in the settings SQLite table
  2. Generates a fresh random 32-byte key using crypto.randomBytes(32) if none exists
  3. Persists the new key to the database for consistency across restarts
  4. Logs a warning that this auto-generated key should not be used in production

This fallback ensures developers can run the application immediately while maintaining the same encryption semantics as production.

The AES-256-GCM Encryption Flow

When a user submits a new provider key via the API, FreeLLMAPI uses the encrypt() function exported from server/src/lib/crypto.ts to secure the data before database insertion.

Generating the Initialization Vector and Ciphertext

The encrypt() function creates a unique cryptographic context for every key:

export function encrypt(text: string): { encrypted: string; iv: string; authTag: string } {
  const key = getEncryptionKey();               // 32-byte Buffer
  const iv = crypto.randomBytes(16);            // 16-byte IV
  const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);

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

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

Key implementation details:

  • Unique IV: Every encryption call generates a fresh 16-byte random initialization vector, ensuring that identical keys produce different ciphertexts
  • GCM Mode: Uses Node.js crypto.createCipheriv with the aes-256-gcm algorithm, providing both confidentiality and authentication
  • Hex Encoding: Returns all components as hexadecimal strings for safe storage in SQLite text columns

Storing the Authentication Tag

The Galois/Counter Mode authentication tag is forced to 16 bytes via the internal AUTH_TAG_BYTES constant. This tag is essential for integrity verification during decryption, preventing undetected tampering with the encrypted payload.

Decrypting API Keys for Outbound Requests

When the server needs to sign a request to an LLM provider, it reconstructs the original key using the decrypt() function in server/src/lib/crypto.ts.

Integrity Verification with GCM Tags

The decryption process validates data integrity before returning the plaintext:

export function decrypt(encrypted: string, iv: string, authTag: string): string {
  const key = getEncryptionKey();
  const decipher = crypto.createDecipheriv(
    'aes-256-gcm',
    key,
    Buffer.from(iv, 'hex'),
    { authTagLength: 16 }                      // pinned tag length
  );
  decipher.setAuthTag(Buffer.from(authTag, 'hex'));

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

Security mechanisms:

  • Explicit Tag Length: The authTagLength: 16 option prevents truncation attacks by requiring the full authentication tag
  • Verification: decipher.final() automatically verifies the GCM tag; if the ciphertext, IV, or tag have been tampered with, Node.js throws an error and the request fails
  • Memory Safety: The master key only exists in process memory during the decryption operation

Database Storage and API Integration

The encryption layer integrates directly with the REST API routes defined in server/src/routes/keys.ts and the underlying SQLite schema.

Schema Design in api_keys Table

The database stores three distinct fields for each encrypted key:

  • encrypted_key: The hex-encoded ciphertext
  • iv: The 16-byte initialization vector used during encryption
  • auth_tag: The 16-byte GCM authentication tag

This separation ensures that no single field contains sufficient information to recover the plaintext without the master key.

Encrypting on Insert (POST /keys)

When creating a new key record, the route handler encrypts the raw provider key before persistence:

const { encrypted, iv, authTag } = encrypt(keyToStore);
db.prepare(`
  INSERT INTO api_keys (platform, label, encrypted_key, iv, auth_tag, status, enabled)
  VALUES (?, ?, ?, ?, ?, 'unknown', 1)
`).run(platform, label ?? '', encrypted, iv, authTag);

This pattern ensures that raw keys never touch the database, even temporarily.

Decrypting on Read and Usage

The system decrypts keys in two primary scenarios:

1. Key Listing (GET /keys) For the administrative interface, the API decrypts keys to display masked versions using the maskKey() helper:

const realKey = decrypt(row.encrypted_key, row.iv, row.auth_tag);
const maskedKey = maskKey(realKey); // Shows only first and last characters

2. Outbound Provider Requests When routing requests to LLM providers (in services like media.ts or embeddings.ts), the system retrieves the stored components and decrypts them to populate the Authorization header:

const apiKey = decrypt(row.encrypted_key, row.iv, row.auth_tag);
const response = await fetch(providerEndpoint, {
  headers: { Authorization: `Bearer ${apiKey}` }
});

Security Guarantees and Best Practices

FreeLLMAPI implements several defense-in-depth measures to protect provider credentials:

  • Confidentiality at Rest: AES-256-GCM encryption ensures that database dumps or backups reveal no usable key material without the master ENCRYPTION_KEY
  • Integrity Protection: The 16-byte GCM authentication tag prevents ciphertext tampering; any modification of the stored data causes immediate decryption failure
  • Minimal Exposure: Only masked key representations (maskKey()) are ever sent to clients; full plaintext keys exist only ephemerally in server memory during outbound requests
  • Key Rotation Ready: The master key is loaded once at startup, making rotation as simple as updating ENCRYPTION_KEY and re-encrypting the database contents

Summary

  • FreeLLMAPI uses AES-256-GCM via Node.js crypto module to encrypt all provider API keys before storage
  • The system requires a 32-byte master key (ENCRYPTION_KEY) in production, with automatic generation available only in development
  • Each encryption operation generates a unique 16-byte IV and produces a 16-byte authentication tag, both stored alongside the ciphertext
  • Decryption occurs in server/src/lib/crypto.ts using decrypt() and verifies integrity via the GCM tag before returning plaintext
  • Raw keys are never persisted; only the encrypted triplet (ciphertext, IV, tag) exists in the api_keys table

Frequently Asked Questions

What happens if the ENCRYPTION_KEY environment variable is missing in production?

If ENCRYPTION_KEY is not set in a production environment, the initEncryptionKey() function in server/src/lib/crypto.ts will throw an error during server startup. This fail-safe design prevents the application from running in an insecure state where it cannot encrypt or decrypt API keys.

How does FreeLLMAPI handle key rotation?

FreeLLMAPI stores the master key in memory only, meaning you can rotate the encryption key by updating the ENCRYPTION_KEY environment variable and restarting the server. To re-encrypt existing keys with the new master key, you would need to iterate through the api_keys table, decrypt each entry with the old key, and re-encrypt with the new key.

Is the initialization vector (IV) secret?

No, the IV does not need to remain secret. According to cryptographic best practices implemented in the codebase, the IV is stored plainly in the iv column and transmitted alongside the ciphertext. The IV ensures that encrypting the same key twice produces different outputs, preventing pattern analysis, but it is not a secret key component.

What encryption algorithm does FreeLLMAPI use specifically?

FreeLLMAPI uses AES-256-GCM (Advanced Encryption Standard with 256-bit keys in Galois/Counter Mode), as specified in the crypto.createCipheriv('aes-256-gcm', ...) calls within server/src/lib/crypto.ts. This mode provides both authenticated encryption (confidentiality) and integrity verification (tamper detection) in a single operation.

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 →