How FreeLLMAPI Stores and Encrypts Provider API Keys
FreeLLMAPI stores provider API keys in a SQLite database using AES-256-GCM encryption with unique initialization vectors and authentication tags for each key, ensuring credentials remain encrypted at rest and are only decrypted in memory during request processing.
FreeLLMAPI is an open-source LLM gateway that aggregates multiple AI providers under a unified API. To protect sensitive credentials, the application implements a robust encryption layer in server/src/lib/crypto.ts that ensures provider API keys never exist in plain text on the filesystem or database.
Storage Location and Database Schema
The SQLite Database Structure
FreeLLMAPI persists all provider credentials in a SQLite database created at runtime by better-sqlite3. The specific table responsible for key storage is named api_keys, defined in the migration file server/src/db/migrations/20260101_000000_legacy_baseline.ts.
The schema stores metadata alongside the encrypted payload:
platform: Provider identifier (e.g.,openai,google,custom)label: Human-readable description for the keyencrypted_key: The AES-encrypted API key ciphertextiv: The 12-byte initialization vector used for encryptionauth_tag: The GCM authentication tag ensuring data integritystatus: Health status (healthy,unknown, etc.)enabled: Boolean flag indicating if the key is active
This design ensures that even if the database file is compromised, the keys remain inaccessible without the ENCRYPTION_KEY environment variable.
Encryption Implementation
The encryption logic resides in server/src/lib/crypto.ts and uses Node.js's native crypto module.
AES-256-GCM Configuration
Each API key undergoes AES-256-GCM encryption using a unique 12-byte initialization vector generated for every encryption operation. The implementation in server/src/lib/crypto.ts follows this pattern:
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
const secret = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex');
export function encrypt(plain: string) {
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', secret, iv);
const encrypted = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]);
const authTag = cipher.getAuthTag();
return {
encryptedKey: encrypted.toString('base64'),
iv: iv.toString('base64'),
authTag: authTag.toString('base64')
};
}
The authenticated encryption mode (GCM) provides both confidentiality and integrity verification through the auth_tag parameter.
Environment-Based Secret Key
The symmetric encryption key is sourced from the ENCRYPTION_KEY environment variable, as documented in .env.example. This value must be a 64-character hexadecimal string (32 bytes) representing the AES-256 key. The secret never commits to version control and must be supplied at runtime, ensuring separation of code and credentials.
Key Lifecycle Flow
Storing New API Keys
When users add a provider key through the API endpoint defined in server/src/routes/keys.ts, the system immediately encrypts the plaintext before database insertion:
// Receiving plaintext key from request body
const { encryptedKey, iv, authTag } = encrypt(plainKey);
db.prepare(`
INSERT INTO api_keys (platform, label, encrypted_key, iv, auth_tag, status, enabled)
VALUES (?, ?, ?, ?, ?, 'unknown', 1)
`).run(platform, label, encryptedKey, iv, authTag);
This ensures that the raw API key exists only transiently in memory during the request handling.
Retrieving and Decrypting Keys at Runtime
During request routing, server/src/services/router.ts retrieves the appropriate key and decrypts it for the provider call:
const row = db.prepare(`
SELECT encrypted_key, iv, auth_tag FROM api_keys
WHERE platform = ? AND enabled = 1 AND status IN ('healthy','unknown')
LIMIT 1
`).get(platform);
const apiKey = decrypt(row.encrypted_key, row.iv, row.auth_tag);
// apiKey now contains the clear-text value for the provider client
The decrypt function in server/src/lib/crypto.ts validates the authentication tag before returning the plaintext:
export function decrypt(enc: string, ivB64: string, tagB64: string) {
const iv = Buffer.from(ivB64, 'base64');
const authTag = Buffer.from(tagB64, 'base64');
const decipher = createDecipheriv('aes-256-gcm', secret, iv);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([
decipher.update(Buffer.from(enc, 'base64')),
decipher.final()
]);
return decrypted.toString('utf8');
}
Summary
- Provider API keys are stored in a SQLite
api_keystable managed bybetter-sqlite3inserver/src/db/index.ts - Encryption uses AES-256-GCM with unique 12-byte IVs and authentication tags for every key, implemented in
server/src/lib/crypto.ts - Key material derives from the
ENCRYPTION_KEYenvironment variable (hex-encoded 32-byte secret) defined in.env.example - Clear-text keys exist only in memory during active requests; they are encrypted before database storage and decrypted on-demand in
server/src/services/router.ts - Integrity protection is provided by GCM authentication tags stored alongside ciphertext in
auth_tagandencrypted_keycolumns
Frequently Asked Questions
Where does FreeLLMAPI store the encryption key for API credentials?
The encryption key is not stored in the database. FreeLLMAPI reads the ENCRYPTION_KEY environment variable at runtime from the host system. This must be a 64-character hexadecimal string representing a 32-byte AES-256 key. The application fails to start if this variable is missing, ensuring keys cannot be decrypted without explicit configuration.
Can I rotate the encryption key without losing my stored API keys?
No, standard key rotation requires re-encryption. Since FreeLLMAPI uses symmetric AES-256-GCM encryption, changing the ENCRYPTION_KEY environment variable renders existing ciphertexts undecryptable. To rotate keys, you must decrypt all existing entries with the old key, re-encrypt them with the new key, and update the database records. The current implementation does not include automatic key rotation helpers.
Why does FreeLLMAPI use AES-256-GCM instead of AES-256-CBC?
FreeLLMAPI selects AES-256-GCM because it provides authenticated encryption with associated data (AEAD). Unlike CBC mode, GCM includes an authentication tag (auth_tag) that verifies both data integrity and authenticity, preventing tampering with ciphertexts. This protection is critical for API keys, ensuring that database corruption or malicious modification of encrypted values is detected during decryption.
How does the application handle decryption failures?
When server/src/lib/crypto.ts encounters an invalid ciphertext, wrong authentication tag, or corrupted IV, the Node.js crypto module throws an error during decipher.final(). This error propagates to the caller in server/src/services/router.ts, which typically results in a failed request and an error log. The application does not expose the specific decryption failure to end users to prevent information leakage about the encryption scheme.
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 →