How FreeLLMAPI Implements API Key Encryption: AES-256-GCM Security Deep Dive
FreeLLMAPI encrypts all provider API keys using AES-256-GCM with a 64-character hex encryption key stored in the ENCRYPTION_KEY environment variable, persisting the ciphertext, 16-byte IV, and 16-byte auth tag in SQLite while never exposing plaintext keys to clients.
FreeLLMAPI is an open-source LLM gateway that aggregates multiple AI providers behind a unified API. Because the service stores sensitive third-party credentials, it implements API key encryption using Node.js's native crypto module. The entire cryptographic workflow is isolated in server/src/lib/crypto.ts and employs authenticated encryption to ensure secrets remain protected at rest.
Encryption Architecture and Key Initialization
The security model begins with master key management in server/src/lib/crypto.ts. This module handles both key derivation and cryptographic operations through two distinct modes.
Production key sourcing: In production environments, FreeLLMAPI expects a hex-encoded 64-character (32-byte) key supplied via the ENCRYPTION_KEY environment variable. The initEncryptionKey() function validates this using parseHexKey(), which enforces exact byte length and hexadecimal format to prevent weak or malformed configurations.
Development fallback: When NODE_ENV is not production and no valid key is detected, the system auto-generates a cryptographically random key, persists it to the local settings table, and reuses it for the lifetime of that database instance. This ensures zero-configuration local development while making it obvious that production deployments require explicit key management.
// 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');
return;
}
// dev fallback: read from DB or generate new key
// ...
}
The Encryption Process (AES-256-GCM)
When users add new provider keys via POST /keys or POST /keys/custom, the plaintext never touches the database directly. Instead, the encrypt() function performs authenticated encryption:
// server/src/lib/crypto.ts
export function encrypt(text: string): { encrypted: string; iv: string; authTag: string } {
const key = getEncryptionKey();
const iv = crypto.randomBytes(16);
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 };
}
The implementation uses:
- Algorithm:
aes-256-gcmfor authenticated encryption with associated data (AEAD) - IV: 16 cryptographically random bytes (128 bits) generated via
crypto.randomBytes(16) - Auth Tag: 16 bytes (
AUTH_TAG_BYTES = 16) to prevent truncation attacks
The resulting three components—encrypted, iv, and authTag—are stored in the api_keys table in their respective columns (encrypted_key, iv, auth_tag).
The Decryption Workflow
Whenever backend services need the real provider credentials—such as in server/src/services/router.ts, media.ts, embeddings.ts, or health.ts—they retrieve the stored components and invoke 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;
}
The decryption process validates integrity through GCM's built-in authentication. If the authentication tag verification fails—indicating tampering, corruption, or key mismatch—the operation throws an error. Routes in server/src/routes/keys.ts catch these failures and return "[decrypt failed]" to prevent information leakage while maintaining API stability.
Security Hardening and Mitigations
FreeLLMAPI implements several defense-in-depth measures within server/src/lib/crypto.ts:
- Strict key validation:
parseHexKey()rejects any key not exactly 32 bytes (64 hex characters) and aborts startup if the format is invalid. - Placeholder protection: The code explicitly refuses to use the placeholder string
your-64-char-hex-key-herein production environments. - Truncation attack prevention: Setting
AUTH_TAG_BYTES = 16forces full-size authentication tags, closing vulnerability windows where truncated tags might be accepted by older OpenSSL versions. - Logging safety: Only masked keys (via
maskKey()) ever leave the server process; plaintext keys are never serialized to logs or HTTP responses.
End-to-End Key Lifecycle
The complete flow spans multiple files across the codebase:
- Startup:
initEncryptionKey()runs immediately after database initialization inserver/src/app.ts. - Storage: Routes in
server/src/routes/keys.tsencrypt incoming plaintext before executing INSERT operations against theapi_keystable. - Retrieval: Service layers decrypt on-demand when routing requests to upstream providers, keeping plaintext only in process memory.
- Rotation: Updates trigger re-encryption with fresh IVs; deletions permanently remove the encrypted material from the database.
By isolating all cryptographic logic in server/src/lib/crypto.ts, the codebase remains auditable and supports straightforward algorithm replacement if future security requirements demand it.
Summary
- FreeLLMAPI uses AES-256-GCM authenticated encryption via Node.js
cryptofor all API key storage at rest. - Production requires a 64-character hex
ENCRYPTION_KEYenvironment variable; development auto-generates ephemeral keys persisted to SQLite. - Encryption produces three stored components per key: ciphertext, 16-byte IV, and 16-byte auth tag in the
api_keystable columns. - Decryption occurs only in backend service layers (
router.ts,media.ts,embeddings.ts,health.ts) and never exposes plaintext to API clients. - Security controls prevent weak keys, placeholder usage in production, truncation attacks, and accidental credential logging.
Frequently Asked Questions
What encryption algorithm does FreeLLMAPI use?
FreeLLMAPI uses AES-256-GCM (Galois/Counter Mode) as implemented in Node.js's crypto module. This authenticated encryption mode provides both confidentiality and integrity verification through a 16-byte authentication tag generated during encryption and validated during decryption in server/src/lib/crypto.ts.
How is the master encryption key managed?
In production, FreeLLMAPI requires the ENCRYPTION_KEY environment variable set to a 64-character hexadecimal string representing 32 bytes of entropy. The initEncryptionKey() function validates this strictly using parseHexKey() and refuses to start if the placeholder value is detected. During development, the system automatically generates a random key, stores it in the local SQLite settings table, and reuses it until the database is reset.
Can encrypted API keys be leaked through the REST API?
No. The server/src/routes/keys.ts endpoints decrypt stored credentials only to pass them through maskKey(), which returns truncated representations (such as sk-...7890) to clients. The plaintext key exists only briefly in memory during active decryption within backend service layers, and the AES-GCM authentication tag prevents undetected tampering with stored ciphertext.
What happens if decryption fails?
If decrypt() encounters corrupted data, an invalid authentication tag, or a mismatched encryption key, the function throws an error that calling code catches. For example, when listing keys in server/src/routes/keys.ts, decryption failures result in the placeholder string "[decrypt failed]" being returned instead of crashing the server or exposing stack traces that could reveal implementation details.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →