How API Keys Are Encrypted and Stored in FreeLLMAPI: AES-256-GCM Implementation
FreeLLMAPI protects every provider API key using AES-256-GCM encryption, storing only the encrypted ciphertext, random IV, and authentication tag in SQLite while decrypting credentials only in memory during active requests.
FreeLLMAPI implements a zero-trust "encrypt-at-rest" architecture that safeguards third-party LLM credentials from database breaches. Understanding exactly how API keys are encrypted and stored in FreeLLMAPI reveals a security model where the encryption key never leaves the server process, ensuring raw provider tokens remain inaccessible even if the underlying storage is compromised. The system uses Node.js native crypto modules and a unified bearer token abstraction to isolate client applications from provider-specific secrets.
The AES-256-GCM Encryption Pipeline
FreeLLMAPI employs AES-256-GCM (Galois/Counter Mode) for authenticated encryption. This provides both confidentiality and integrity verification, ensuring stored keys cannot be tampered with or read without the master secret.
Key Derivation and Environment Configuration
On server startup, FreeLLMAPI initializes its encryption context by either generating a fresh 256-bit secret or reading the ENCRYPTION_KEY variable from the environment file. This secret remains resident only in process memory and is never persisted to disk. As implemented in the server bootstrap logic, the application fails safe if neither a generated nor configured key is available.
Per-Key Encryption Process
When a new provider key arrives via the UI or the /keys REST endpoint defined in server/src/routes/keys.ts, the server executes the following cryptographic sequence:
- Generates a cryptographically random IV using
crypto.randomBytes(12)to ensure unique ciphertexts for identical keys. - Creates the cipher via
crypto.createCipheriv('aes-256-gcm', secret, iv), binding the 256-bit master secret to the per-key IV. - Encrypts the raw provider key, extracting both the
encrypted_keyciphertext and the 16-byteauth_tag(authentication tag) required for GCM verification. - Persists the three components—
encrypted_key,iv, andauth_tag—alongside the platform label into the SQLite database viaserver/src/services/declarative-config.ts.
The raw provider key exists only as a transient variable during this encryption routine and is immediately cleared from memory after the database transaction commits.
Database Storage Schema
FreeLLMAPI stores encrypted credentials in a dedicated SQLite table with a schema designed to prevent data leakage. The migration file server/src/db/migrations/20260805_000002_client_profiles.ts defines the api_keys table structure, explicitly separating the encrypted payload from its non-sensitive metadata.
The api_keys Table Structure
Each row contains:
encrypted_key: The AES-256-GCM ciphertext (binary/blob format).iv: The 12-byte initialization vector unique to this encryption operation.auth_tag: The authentication tag proving ciphertext integrity.platform: Human-readable label (e.g., "openai", "anthropic") stored in plaintext.label: User-defined description for key management.
Because the database contains only these three cryptographic pieces—and never the master ENCRYPTION_KEY—a compromised SQLite file yields no usable credentials to an attacker.
Runtime Decryption and Memory Safety
During request handling, the router logic in server/src/services/router.ts performs just-in-time decryption to inject provider keys into upstream LLM API calls. This design minimizes the window of exposure for plaintext secrets.
Per-Request Decryption Flow
For each incoming inference request:
- The router retrieves the specific row (
encrypted_key,iv,auth_tag) from theapi_keystable based on the requested model provider. - It invokes the internal
decrypthelper, which reconstructs the decipher usingcrypto.createDecipheriv('aes-256-gcm', secret, iv)with the same master secret and stored IV. - The function calls
decipher.final()and verifies theauth_tagagainst the stored value; mismatching tags immediately throw integrity errors.
The plaintext key resides only in memory for the duration of the request lifecycle and is explicitly dereferenced before the HTTP response returns. The system never writes decrypted keys to logs, disk, or error traces.
Memory Safety Guarantees
FreeLLMAPI leverages JavaScript's scoped variable handling and explicit buffer clearing where possible to prevent key material from persisting in memory pools (heaps) beyond the active request. Because decryption happens inside server/src/services/router.ts and not in the public-facing route handlers, client applications interact solely with the unified FreeLLMAPI bearer token, never touching the underlying provider secrets.
Managing Encrypted Keys via REST API
The public API in server/src/routes/keys.ts provides endpoints for adding and listing keys while maintaining the encryption abstraction.
Adding a Provider Key
To store a new encrypted credential:
curl -X POST http://localhost:3001/v1/keys \
-H "Authorization: Bearer $(cat .env | grep ENCRYPTION_KEY | cut -d= -f2)" \
-H "Content-Type: application/json" \
-d '{
"platform": "openai",
"label": "My OpenAI Key",
"apiKey": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}'
The server encrypts the apiKey field and stores the encrypted_key, iv, and auth_tag in SQLite. The raw token never appears in server logs.
Retrieving Masked Keys
Client applications fetch metadata without accessing encrypted payloads:
curl http://localhost:3001/v1/keys/1 \
-H "Authorization: Bearer <unified-token>"
Response:
{
"id": 1,
"platform": "openai",
"label": "My OpenAI Key",
"maskedKey": "sk-••••••••••••••••••••••••••••••"
}
Using the Unified Token
Client SDKs send only the FreeLLMAPI bearer token. The router decrypts the underlying provider key on-the-fly:
export OPENAI_API_KEY=$(curl -s http://localhost:3001/v1/keys | jq -r .unifiedKey)
openai api chat.completions.create -m gpt-4o -g "Hello!"
The provider-specific secret remains encrypted at rest and is only decrypted within the server/src/services/router.ts process boundary during the active API call.
Summary
FreeLLMAPI implements a defense-in-depth strategy for credential storage:
- AES-256-GCM encryption provides authenticated confidentiality for all provider keys.
- Three-part storage (
encrypted_key,iv,auth_tag) in the SQLiteapi_keystable ensures database dumps reveal no usable secrets. - Just-in-time decryption inside
server/src/services/router.tskeeps plaintext keys only in volatile memory for the duration of requests. - Master key isolation via the
ENCRYPTION_KEYenvironment variable prevents decryption by anyone without server process access. - Unified bearer token abstraction isolates client applications from provider-specific credentials.
Frequently Asked Questions
What encryption algorithm does FreeLLMAPI use for API keys?
FreeLLMAPI uses AES-256-GCM (Galois/Counter Mode) authenticated encryption. This algorithm combines the AES block cipher with 256-bit keys and GCM mode to provide both confidentiality and integrity verification via authentication tags.
Where is the master encryption key stored?
The master encryption key is either generated as a 256-bit random secret on server startup or loaded from the ENCRYPTION_KEY environment variable defined in the .env file. According to the source code in server/src/services/declarative-config.ts, this key never leaves process memory and is never written to the SQLite database.
Can someone read my API keys if they steal the database file?
No. The SQLite database contains only the encrypted_key ciphertext, iv, and auth_tag for each credential. Without the master ENCRYPTION_KEY that resides only in the server's environment or memory, the stored cryptographic material cannot be decrypted into usable provider tokens.
How long do decrypted keys remain in memory?
Decrypted keys exist only for the duration of an active request. The server/src/services/router.ts implementation decrypts the provider key immediately before calling the upstream LLM API and explicitly dereferences the plaintext variable before returning the response, ensuring keys are not retained in memory between requests or written to logs.
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 →