How API Keys Are Stored and Encrypted in FreeLLMAPI: AES-256-GCM Implementation

FreeLLMAPI encrypts every provider API key using AES-256-GCM with a 32-byte encryption key stored separately from the database, ensuring credentials remain encrypted at rest and are only decrypted in memory during active requests.

The FreeLLMAPI repository implements a comprehensive encryption scheme for protecting sensitive provider credentials. Unlike simple hashing or base64 encoding, the project uses industry-standard AES-256-GCM authenticated encryption with unique initialization vectors per key, ensuring both confidentiality and integrity of stored API keys.

Encryption Architecture Overview

FreeLLMAPI stores all provider credentials—including API keys and custom proxy URLs—in an SQLite database with encrypted values only. The plaintext never persists to disk at any point.

The encryption system centers on three core design principles:

  • Separation of encryption key and data: The ENCRYPTION_KEY lives outside the database entirely
  • Authenticated encryption: GCM mode provides tamper detection via authentication tags
  • Ephemeral decryption: Keys exist in plaintext only for the duration of a single request

Core Encryption Module: server/src/lib/crypto.ts

The crypto.ts file contains the entire encryption implementation. Here is the actual encryption flow used when storing a new API key:

// Encrypt a raw provider key before persisting
import { encrypt } from './server/src/lib/crypto.js';

const rawKey = 'sk-abc123…';
const { encrypted, iv, authTag } = encrypt(rawKey);

// Store in SQLite (pseudo-code)
db.prepare(`
  INSERT INTO api_keys (platform, label, encrypted_key, iv, auth_tag)
  VALUES (?, ?, ?, ?, ?)
`).run('openai', 'my-key', encrypted, iv, authTag);

The encrypt() function returns an object with three components:

  1. encrypted – The ciphertext in hexadecimal format
  2. iv – A 16-byte initialization vector (hex-encoded), unique per encryption operation
  3. authTag – The GCM authentication tag (hex-encoded) for integrity verification

Decryption for Active Requests

When a request requires a provider key, the system retrieves the encrypted components and decrypts them in memory:

// Decrypt a stored key for an outgoing request
import { decrypt } from './server/src/lib/crypto.js';

const row = db.prepare(`
  SELECT encrypted_key, iv, auth_tag FROM api_keys WHERE id = ?
`).get(keyId);

const plaintextKey = decrypt(row.encrypted_key, row.iv, row.auth_tag);
// Use `plaintextKey` as the Authorization header

The plaintext key exists only in memory for the duration of the request and is never logged or cached.

Encryption Key Management

The 32-byte encryption key (hex-encoded as 64 characters) is sourced through a tiered fallback mechanism:

  1. Primary: Environment variable ENCRYPTION_KEY
  2. Development fallback: Auto-generated random key stored in .encryption-key file

Development Key Generation

When ENCRYPTION_KEY is unset, the server creates a .encryption-key file with strict permissions:

  • File mode 0600 (owner read/write only)
  • Placement adjacent to the SQLite database file
  • Ownership restricted to the process user

This key file never contains database data—it only holds the encryption key itself, maintaining proper separation.

Key Fingerprinting for Verification

For operational diagnostics without key exposure, the crypto module provides encryptionKeyFingerprint():

// Returns SHA-256 hash of the encryption key
const fingerprint = encryptionKeyFingerprint();

This allows administrators to verify that a database backup or migration uses the expected encryption key without revealing the key itself.

Database Schema and Storage

The api_keys table schema stores encrypted components in separate columns, as defined in the migration files under server/src/db/migrations/:

Column Purpose
encrypted_key AES-256-GCM ciphertext (hex)
iv 16-byte initialization vector (hex)
auth_tag GCM authentication tag (hex)

Additional columns proxy_encrypted, proxy_iv, and proxy_auth_tag store encrypted custom proxy URLs using the same pattern.

API Route Implementation: server/src/routes/keys.ts

The /keys endpoint handles incoming key submissions. According to the source code in server/src/routes/keys.ts, the route:

  1. Receives the plaintext key from UI or API client
  2. Calls encrypt(keyValue) from the crypto module
  3. Inserts the three encrypted components into api_keys

No plaintext key ever reaches the database layer.

Key Masking for UI Safety

To prevent accidental exposure in administrative interfaces, maskKey() in crypto.ts generates display-safe versions:

// Mask a key for UI display
import { maskKey } from './server/src/lib/crypto.js';

const masked = maskKey('sk-abc123…'); // → "sk-a…c123"

The masking logic shows:

  • First 4 and last 4 characters for keys longer than 12 characters
  • Generic "****" for shorter keys

The UI never receives real keys—only masked representations or encrypted storage operations.

Proxy URL Encryption: server/src/lib/key-proxy.ts

Custom proxy URLs receive identical protection via encryptProxyUrl() and decryptProxyUrl() in server/src/lib/key-proxy.ts. These functions wrap the same AES-256-GCM primitives, storing results in the proxy_* column variants.

Security Documentation References

The repository includes dedicated security documentation:

Summary

  • Algorithm: AES-256-GCM authenticated encryption with unique IVs per key
  • Key storage: Environment variable ENCRYPTION_KEY or .encryption-key file with mode 0600
  • Database storage: Three-column schema (encrypted_key, iv, auth_tag) in SQLite
  • Runtime behavior: Decryption occurs in memory only, for the duration of each request
  • UI protection: maskKey() prevents accidental exposure in interfaces
  • Proxy support: Identical encryption flow via server/src/lib/key-proxy.ts

FreeLLMAPI's encryption implementation follows cryptography best practices by separating keys from data, using authenticated encryption, and minimizing plaintext exposure to absolute runtime necessity.

Frequently Asked Questions

What encryption algorithm does FreeLLMAPI use for API keys?

FreeLLMAPI uses AES-256-GCM, an authenticated encryption mode that provides both confidentiality and integrity verification. The implementation requires a 32-byte key hex-encoded as 64 characters, with unique 16-byte initialization vectors generated for each encryption operation.

Where is the encryption key stored in FreeLLMAPI?

The encryption key is never stored in the database. It is sourced from the ENCRYPTION_KEY environment variable, or auto-generated into a .encryption-key file with 0600 permissions in development environments. This separation ensures that database compromise alone cannot decrypt stored credentials.

How does FreeLLMAPI prevent API keys from appearing in logs or UI?

FreeLLMAPI uses the maskKey() function to generate display-safe versions showing only first/last characters. The actual decrypt() operation occurs only when building outbound provider requests, and plaintext keys exist in memory only for the duration of that specific request cycle.

Can proxy URLs be encrypted the same way as API keys?

Yes. The server/src/lib/key-proxy.ts module provides encryptProxyUrl() and decryptProxyUrl() functions that use identical AES-256-GCM encryption, storing results in dedicated columns (proxy_encrypted, proxy_iv, proxy_auth_tag) within the same api_keys table.

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 →