How AES-256-GCM Encryption Works for Stored API Keys in FreeLLMAPI

FreeLLMAPI uses AES-256-GCM authenticated encryption to ensure provider API keys are never stored in plain text, generating a random 16-byte IV for each encryption operation and verifying integrity via a pinned 16-byte authentication tag during decryption.

FreeLLMAPI is an open-source LLM routing gateway that protects sensitive third-party credentials using industry-standard symmetric encryption. When administrators add provider API keys through the dashboard, the system immediately encrypts them using a 256-bit key before persisting to the SQLite database. This implementation, located in server/src/lib/crypto.ts, ensures that credentials remain confidential and tamper-evident while at rest.

Initializing the 256-Bit Encryption Key

On server startup, the initEncryptionKey() function validates and loads the master encryption key. In production deployments, you must supply a 64-character hex string via the ENCRYPTION_KEY environment variable, which the system decodes to exactly 32 bytes (256 bits) as defined by the constant KEY_BYTES = 32.

The validation logic in lines 8-30 ensures the key meets the KEY_HEX_LEN = 64 requirement, rejecting malformed inputs. A placeholder check in lines 54-58 prevents accidental deployment with default or example credentials. For local development, the system auto-generates a cryptographically secure fallback key, persists it in the SQLite settings table, and reuses it across restarts to maintain data accessibility without requiring manual configuration.

Encrypting API Keys with AES-256-GCM

When a new provider key is saved through the dashboard, the encrypt() function executes a four-step authenticated encryption process. First, it generates a fresh 16-byte initialization vector (IV) using crypto.randomBytes(16) to ensure uniqueness across encryptions. It then initializes a cipher via crypto.createCipheriv('aes-256-gcm', key, iv) and processes the plaintext API key.

After encryption completes, the function extracts the 16-byte authentication tag using cipher.getAuthTag(). This tag acts as a cryptographic checksum. The function returns an object containing the hex-encoded ciphertext, IV, and auth tag, which are then stored in the api_keys table. This implementation appears in lines 85-99 of server/src/lib/crypto.ts.

import { encrypt } from '@/lib/crypto';

// Encrypt a provider API key before database storage
const plain = 'sk-my-google-key';
const { encrypted, iv, authTag } = encrypt(plain);

// Store the three hex strings in the database
db.prepare(`
  INSERT INTO api_keys (provider, encrypted_key, iv, auth_tag)
  VALUES (?, ?, ?, ?)
`).run('google', encrypted, iv, authTag);

Decrypting Keys and Verifying Integrity

To retrieve a stored key for an outbound request, the decrypt() function reconstructs the decipher using crypto.createDecipheriv('aes-256-gcm', key, iv, { authTagLength: 16 }). It explicitly sets the authentication tag with decipher.setAuthTag(), converting the stored hex string back to bytes.

During decryption, OpenSSL automatically verifies that the authentication tag matches the ciphertext. If any bit of the encrypted data, IV, or auth tag has been modified, the decryption throws an error before returning plaintext. This integrity verification, implemented in lines 107-115, ensures that compromised database files cannot be silently altered to inject malicious keys.

import { decrypt } from '@/lib/crypto';

// Retrieve and decrypt a stored provider key
const row = db
  .prepare('SELECT encrypted_key, iv, auth_tag FROM api_keys WHERE provider = ?')
  .get('google');

const plainKey = decrypt(row.encrypted_key, row.iv, row.auth_tag);
// plainKey now contains the original API credential

Security Hardening Against Shortened-Tag Attacks

The implementation explicitly pins the GCM authentication tag length to 16 bytes (AUTH_TAG_BYTES = 16) as shown in lines 101-105. This mitigates the shortened-tag attack vulnerability that Node.js allows by default, where the crypto module accepts tags between 4 and 16 bytes. By enforcing the full 16-byte tag through the authTagLength option in createDecipheriv, FreeLLMAPI eliminates the risk of authentication bypass through tag truncation.

import Database from 'better-sqlite3';
import { initEncryptionKey } from '@/lib/crypto';

// Initialize encryption subsystem on server startup
const db = new Database('data/freellmapi.db');
initEncryptionKey(db);  // Loads ENCRYPTION_KEY from env or generates dev fallback

Summary

  • AES-256-GCM provides both confidentiality and integrity, ensuring API keys cannot be read or modified without detection.
  • Key management requires a 32-byte (64-character hex) ENCRYPTION_KEY in production, with automatic fallback generation for development environments.
  • Per-key randomization uses a unique 16-byte IV for every encryption operation, preventing pattern analysis.
  • Tamper detection relies on a pinned 16-byte authentication tag; any modification to stored data triggers a decryption failure.
  • Source protection keeps the master key in environment variables or encrypted SQLite rows, never in source control.

Frequently Asked Questions

What is AES-256-GCM and why does FreeLLMAPI use it?

AES-256-GCM (Advanced Encryption Standard with Galois/Counter Mode) is a symmetric authenticated encryption algorithm that provides both data confidentiality and integrity verification. FreeLLMAPI uses it because GCM mode generates an authentication tag during encryption that binds the ciphertext to its IV, allowing the system to detect any unauthorized modifications to stored API keys while maintaining high performance.

How is the encryption key managed in production environments?

In production, the encryption key must be supplied via the ENCRYPTION_KEY environment variable as a 64-character hexadecimal string representing 32 bytes of entropy. The initEncryptionKey() function in server/src/lib/crypto.ts validates this key on startup and rejects any keys that do not match the exact length requirements, preventing runtime errors during cryptographic operations.

What happens if the ENCRYPTION_KEY environment variable is missing?

If ENCRYPTION_KEY is not set, the system enters development mode and automatically generates a secure random key using crypto.randomBytes(32). This key is then persisted in the SQLite settings table and reused across server restarts. While this ensures functionality for local development, production deployments will fail to start if the environment variable is missing or contains a placeholder value, as enforced by the checks in lines 54-58.

How does FreeLLMAPI detect tampering with stored API keys?

The decrypt() function detects tampering through the GCM authentication tag. When decrypting, it sets the expected auth tag using decipher.setAuthTag(). If the stored ciphertext, IV, or auth tag has been altered, the authentication check fails and Node.js throws an error before returning the plaintext. This ensures that attackers cannot modify encrypted keys to redirect requests or extract credentials without immediate detection.

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 →