# How FreeLLMAPI Stores and Encrypts Provider API Keys

> Discover how FreeLLMAPI secures provider API keys using AES-256-GCM encryption in a SQLite database. Learn about encryption at rest and in-memory decryption for secure API access.

- Repository: [Tashfeen/freellmapi](https://github.com/tashfeenahmed/freellmapi)
- Tags: internals
- Published: 2026-06-27

---

**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`](https://github.com/tashfeenahmed/freellmapi/blob/main/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`](https://github.com/tashfeenahmed/freellmapi/blob/main/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 key
- **`encrypted_key`**: The AES-encrypted API key ciphertext
- **`iv`**: The 12-byte initialization vector used for encryption
- **`auth_tag`**: The GCM authentication tag ensuring data integrity
- **`status`**: 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`](https://github.com/tashfeenahmed/freellmapi/blob/main/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`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts) follows this pattern:

```typescript
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`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/routes/keys.ts), the system immediately encrypts the plaintext before database insertion:

```typescript
// 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`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/router.ts) retrieves the appropriate key and decrypts it for the provider call:

```typescript
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`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts) validates the authentication tag before returning the plaintext:

```typescript
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_keys` table managed by `better-sqlite3` in [`server/src/db/index.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/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`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts)
- **Key material** derives from the `ENCRYPTION_KEY` environment 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`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/router.ts)
- **Integrity protection** is provided by GCM authentication tags stored alongside ciphertext in `auth_tag` and `encrypted_key` columns

## 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`](https://github.com/tashfeenahmed/freellmapi/blob/main/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`](https://github.com/tashfeenahmed/freellmapi/blob/main/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.