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

> FreeLLMAPI encrypts API keys with AES-256-GCM using Node.js crypto. Learn how we protect your data with this secure implementation stored in SQLite.

- Repository: [Tashfeen/freellmapi](https://github.com/tashfeenahmed/freellmapi)
- Tags: how-to-guide
- Published: 2026-06-26

---

**FreeLLMAPI encrypts all provider API keys using AES-256-GCM via the Node.js crypto module, storing the ciphertext, initialization vector, and authentication tag in SQLite while requiring a 64-character hex master key from the `ENCRYPTION_KEY` environment variable.**

FreeLLMAPI is an open-source LLM API gateway that securely stores provider credentials using industry-standard encryption. Understanding how API keys are encrypted in FreeLLMAPI is essential for security audits and self-hosted deployments. The implementation isolates all cryptographic logic in [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts), ensuring that sensitive keys never persist in plaintext and are only decrypted transiently in memory when needed.

## Encryption Architecture Overview

The encryption workflow centers on **AES-256-GCM** (Galois/Counter Mode), providing authenticated encryption with associated data (AEAD). Every API key undergoes the following transformation:

1. **Plaintext** → **Ciphertext** + **IV** + **Auth Tag**
2. **Storage** → SQLite `api_keys` table (`encrypted_key`, `iv`, `auth_tag` columns)
3. **Retrieval** → Decryption using the master key cached at startup

The core implementation lives in [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts) and is consumed by the key-management routes ([`server/src/routes/keys.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/routes/keys.ts)) and service layers including the request router, media service, embeddings service, and health checks.

## Initializing the Master Encryption Key

Before any encryption or decryption occurs, the system must initialize a 32-byte master key.

### Production Configuration

In production, FreeLLMAPI expects a hex-encoded 64-character key in the `ENCRYPTION_KEY` environment variable. The `initEncryptionKey()` function validates this input using `parseHexKey()`, which guarantees exactly 32 bytes (64 hex characters) and aborts early if the format is incorrect.

```typescript
// 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');       // ← validate length + hex format
    return;
  }
  // dev fallback → read from DB or generate a new one
  …
}

```

The code explicitly refuses to use placeholder values (such as `your-64-char-hex-key-here`) in production environments.

### Development Fallback

When `NODE_ENV !== 'production'`, the system auto-generates a cryptographically secure key, persists it in the local `settings` table, and reuses it for the lifetime of the database. This temporary key disappears when the database is reset, enforcing the requirement that production deployments must supply a real encryption key.

## Encrypting New API Keys

When a user adds a new provider key via `POST /keys` or `POST /keys/custom`, the plaintext is passed to the `encrypt()` function. This function generates a random 16-byte initialization vector (IV) and produces a 16-byte authentication tag to prevent tampering.

```typescript
// server/src/lib/crypto.ts
export function encrypt(text: string): { encrypted: string; iv: string; authTag: string } {
  const key = getEncryptionKey();                 // ← cached key from initEncryptionKey()
  const iv  = crypto.randomBytes(16);             // 96‑bit nonce
  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 };
}

```

**Key parameters:**
- **Algorithm**: `aes-256-gcm`
- **IV**: 16 random bytes (128 bits), stored as hex
- **Auth Tag**: 16 bytes (`AUTH_TAG_BYTES = 16`), preventing truncation attacks

The resulting `encrypted`, `iv`, and `authTag` strings are inserted into the `api_keys` table. The plaintext never touches the database.

## Decrypting Keys at Runtime

Whenever a request requires the real provider key—such as when routing to LLM providers, processing media, generating embeddings, or running health checks—the stored values are retrieved and fed to `decrypt()`:

```typescript
// 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;
}

```

This routine is used throughout the codebase. For example, in [`server/src/routes/keys.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/routes/keys.ts), the system decrypts keys to display masked versions:

```typescript
// server/src/routes/keys.ts
const realKey = decrypt(row.encrypted_key, row.iv, row.auth_tag);
maskedKey = maskKey(realKey);

```

If decryption fails due to corrupted data or a missing master key, the route falls back to `"[decrypt failed]"` rather than exposing error details.

## Security Hardening Measures

FreeLLMAPI implements several defenses against common cryptographic attacks:

- **Key Validation**: The `parseHexKey` utility enforces exactly 64 hex characters (32 bytes), preventing weak key configurations.
- **Placeholder Rejection**: The initialization logic refuses to encrypt data using default placeholder strings, ensuring production keys are explicitly configured.
- **Auth Tag Integrity**: By setting `AUTH_TAG_BYTES = 16`, the code forces a full-size GCM authentication tag, closing the 4-byte truncation attack vector.
- **Memory Safety**: Only masked versions of keys (via `maskKey()`) are returned to clients; the raw plaintext exists only transiently in server memory during active requests.
- **Isolation**: All encryption logic is confined to [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts), making the surface area auditable and simplifying future algorithm updates.

## Complete Key Lifecycle Example

The following illustrates the end-to-end flow from storing a Google API key to using it in a request:

**1. Store the key (client request):**

```bash
curl -X POST http://localhost:3000/keys \
  -H "Content-Type: application/json" \
  -d '{"platform":"google","key":"sk-abcdef1234567890","label":"My Google key"}'

```

Internally, the server calls `encrypt()` and stores the resulting `encrypted_key`, `iv`, and `auth_tag` in SQLite.

**2. Retrieve masked keys (list view):**

```bash
curl http://localhost:3000/keys

```

The server decrypts each entry, applies `maskKey()` to show only the last four characters, and returns the masked view.

**3. Use in internal routing (service layer):**

```typescript
import { getDb } from '../db/index.js';
import { decrypt } from '../lib/crypto.js';

function getProviderKey(platform: string) {
  const db = getDb();
  const row = db.prepare(`
    SELECT encrypted_key, iv, auth_tag
    FROM api_keys
    WHERE platform = ? AND enabled = 1
    ORDER BY created_at DESC LIMIT 1
  `).get(platform);
  return decrypt(row.encrypted_key, row.iv, row.auth_tag);
}

// Usage in server/src/services/router.ts (line 585)
const apiKey = getProviderKey('google');
// Pass apiKey to provider SDK...

```

## Summary

- **Algorithm**: FreeLLMAPI uses **AES-256-GCM** for authenticated encryption of all provider API keys.
- **Storage**: Encrypted keys are stored in SQLite with separate columns for `encrypted_key`, `iv`, and `auth_tag`.
- **Master Key**: Requires a 64-character hex `ENCRYPTION_KEY` environment variable in production; auto-generates temporary keys in development.
- **Implementation**: Core logic resides in [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts), consumed by routes and services including [`server/src/routes/keys.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/routes/keys.ts) and [`server/src/services/router.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/router.ts).
- **Security**: Enforces full-length authentication tags, rejects placeholder keys, and only exposes masked key representations to clients.

## Frequently Asked Questions

### What encryption algorithm does FreeLLMAPI use?

FreeLLMAPI uses **AES-256-GCM** (Galois/Counter Mode) as implemented in the Node.js `crypto` module. This provides authenticated encryption, ensuring both confidentiality and integrity of the stored API keys.

### How long does the encryption key need to be?

The master encryption key must be exactly **64 hexadecimal characters** (representing 32 bytes). The `parseHexKey()` validation function in [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts) enforces this length and format, rejecting shorter or non-hex keys.

### Where are the encrypted keys stored?

Encrypted keys are stored in the **SQLite** `api_keys` table with three distinct columns: `encrypted_key` (the ciphertext), `iv` (the initialization vector), and `auth_tag` (the GCM authentication tag). The plaintext is never written to disk.

### What happens if the ENCRYPTION_KEY is lost?

If the master key is lost or changed, previously encrypted API keys become **permanently unreadable**. FreeLLMAPI will return `"[decrypt failed]"` when attempting to decrypt affected keys. There is no key recovery mechanism; you must delete and re-add the provider keys with the new master key.