# How FreeLLMAPI Handles API Key Encryption: AES-256-GCM Implementation Explained

> FreeLLMAPI encrypts API keys with AES-256-GCM using Node.js crypto, storing ciphertext securely in SQLite. Learn how this implementation protects your sensitive data in production.

- Repository: [Tashfeen/freellmapi](https://github.com/tashfeenahmed/freellmapi)
- Tags: deep-dive
- Published: 2026-06-29

---

**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 an SQLite database while requiring a 64-character hex encryption key via the `ENCRYPTION_KEY` environment variable in production.**

The open-source FreeLLMAPI repository (`tashfeenahmed/freellmapi`) implements a zero-trust approach to sensitive credential storage. Rather than storing plaintext keys, the system uses Node.js native cryptography to ensure that database breaches do not expose actual provider API tokens. The encryption layer is centralized in [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts) and consumed by key management routes and service layers throughout the application.

## Encryption Architecture Overview

FreeLLMAPI treats API keys as sensitive data requiring **at-rest encryption** before persistence. The architecture separates encryption logic from storage concerns, isolating all cryptographic operations in a dedicated utility module. This design allows the routing layer, media services, and embedding services to decrypt keys only when dispatching requests to external LLM providers.

The system uses **AES-256-GCM** (Galois/Counter Mode), which provides both confidentiality and authenticity. Each encrypted key generates a unique 128-bit initialization vector (IV) and a 128-bit authentication tag, preventing tampering and eliminating patterns that could aid cryptanalysis.

## Key Initialization and Management

Encryption key material is handled differently depending on the runtime environment to balance security with developer experience.

### Production Environment Setup

In production, FreeLLMAPI requires a cryptographically strong key provided via the `ENCRYPTION_KEY` environment variable. The system validates this key strictly before use:

```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');       // validates 64-char hex format
    return;
  }
  // development fallback logic...
}

```

The `parseHexKey` function enforces exactly **64 hexadecimal characters** (32 bytes), aborting immediately if the format is invalid. The code also rejects placeholder values like `your-64-char-hex-key-here` to prevent accidental deployment with demo credentials.

### Development Fallback

When `NODE_ENV` is not set to `production`, the system auto-generates a secure random key and persists it in the local SQLite `settings` table. This ephemeral key lasts for the lifetime of the local database clone, ensuring developers never commit production secrets while maintaining encryption functionality.

## The Encryption Process

When users add a new provider key via endpoints like `POST /keys` or `POST /keys/custom`, the plaintext undergoes transformation through the `encrypt()` function:

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

```

The function generates three distinct outputs:
- **encrypted**: The hex-encoded ciphertext
- **iv**: The 16-byte initialization vector (hex-encoded)
- **authTag**: The 16-byte GCM authentication tag (hex-encoded)

These values are stored in the `api_keys` table columns `encrypted_key`, `iv`, and `auth_tag` respectively.

## Decryption and Usage

Service layers retrieve provider credentials by fetching the stored encryption components and passing them to the `decrypt()` function:

```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 decryption routine is invoked 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);

```

Similarly, [`server/src/services/router.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/router.ts), [`server/src/services/media.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/media.ts), and [`server/src/services/embeddings.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/embeddings.ts) decrypt keys immediately before dispatching requests to external APIs. If decryption fails due to corrupted data or key mismatches, the system returns `"[decrypt failed]"` rather than crashing.

## Security Implementation Details

The implementation addresses several attack vectors through strict validation:

| Security Concern | Mitigation Strategy |
|-----------------|---------------------|
| **Key length/format** | `parseHexKey` enforces exactly 32 bytes (64 hex chars) with format validation |
| **Placeholder keys** | Explicit rejection of demo keys in production environments |
| **Auth tag truncation** | `AUTH_TAG_BYTES = 16` forces full-size 128-bit tags, preventing 4-byte truncation attacks |
| **Information leakage** | Only masked keys (`maskKey`) return to clients; plaintext never leaves the server process |
| **Database exposure** | SQLite stores only ciphertext; the encryption key resides solely in memory or environment variables |

## End-to-End Workflow

The complete lifecycle of API key encryption follows this sequence:

1. **Startup**: `initEncryptionKey()` validates the environment variable or generates a development key after the database connection opens in [`server/src/app.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/app.ts)
2. **Storage**: Adding a key triggers `encrypt()`, which inserts `encrypted_key`, `iv`, and `auth_tag` into the `api_keys` table via [`server/src/routes/keys.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/routes/keys.ts)
3. **Retrieval**: Service layers query the database and call `decrypt()` to obtain plaintext for SDK authentication
4. **Rotation**: Updating a key repeats the encryption process, while deletions remove the ciphertext row entirely

## Code Examples

### Adding a New Provider Key

```typescript
import fetch from 'node-fetch';

await fetch('http://localhost:3000/keys', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    platform: 'google',
    key: 'sk-abcdef1234567890',
    label: 'My Google key'
  })
});

```

Internally, the route calls `encrypt()` and stores the resulting components in SQLite.

### Retrieving Masked Keys

```typescript
const res = await fetch('http://localhost:3000/keys');
const keys = await res.json();
console.log(keys);
/* Output:
[
  {
    id: 3,
    platform: 'google',
    label: 'My Google key',
    maskedKey: 'sk-...7890',
    enabled: true
  }
]
*/

```

The server decrypts entries internally but only exposes masked versions via the API.

### Using Stored Keys in Service Calls

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

function getProviderKey(platform: string): 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
const apiKey = getProviderKey('google');
await fetch('https://generativelanguage.googleapis.com/v1/...', {
  headers: { Authorization: `Bearer ${apiKey}` }
});

```

## Summary

- **FreeLLMAPI** uses **AES-256-GCM** encryption via Node.js `crypto` to secure API keys at rest in SQLite
- Encryption keys must be provided as **64-character hex strings** via the `ENCRYPTION_KEY` environment variable in production
- The system stores three components per key: `encrypted_key`, `iv`, and `auth_tag` in the `api_keys` table
- All cryptographic logic is isolated in **[`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts)**, consumed by routes and services that need to decrypt credentials
- **Security features** include placeholder key rejection, full-size authentication tags, and automatic key masking in API responses

## Frequently Asked Questions

### What encryption algorithm does FreeLLMAPI use for API keys?

FreeLLMAPI uses **AES-256-GCM** (Advanced Encryption Standard in Galois/Counter Mode). This algorithm provides both confidentiality and authentication, ensuring that tampered ciphertext is detected during decryption. The implementation uses Node.js's native `crypto` module with a 256-bit key, 128-bit IV, and 128-bit authentication tag.

### How do I configure the encryption key for production deployment?

Set the `ENCRYPTION_KEY` environment variable to a **64-character hexadecimal string** (representing 32 bytes) before starting the server. The application validates this key format during startup in `initEncryptionKey()` and will abort if the key is missing, improperly formatted, or set to the placeholder value. Development environments automatically generate keys, but production requires explicit configuration to prevent data loss.

### Where exactly are the encrypted API keys stored?

Encrypted keys reside in the **SQLite database** in the `api_keys` table, specifically across three columns: `encrypted_key` (ciphertext), `iv` (initialization vector), and `auth_tag` (authentication tag). The encryption key itself is never stored in the database; it lives only in the server process memory (cached after initialization) or the environment variables.

### Can I rotate the encryption key without losing existing API keys?

FreeLLMAPI does not currently implement automatic re-encryption for key rotation. To rotate the master encryption key, you would need to decrypt all existing entries using the old key and re-encrypt them with the new key. Because the application caches the encryption key at startup, rotating the environment variable requires a server restart to take effect, but existing encrypted data remains accessible only with the original key used during encryption.