# How FreeLLMAPI Encrypts API Keys at Rest with AES-256-GCM and Decrypts Them for Requests

> FreeLLMAPI secures API keys with AES-256-GCM encryption at rest and decrypts them for requests. Discover how FreeLLMAPI protects your credentials using a master secret and SQLite.

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

---

**FreeLLMAPI encrypts provider API keys using AES-256-GCM with a 32-byte master secret, storing the ciphertext, initialization vector, and authentication tag in SQLite, then decrypts them on-demand for outbound requests using the same master key.**

FreeLLMAPI is an open-source API gateway that secures LLM provider credentials by never storing raw API keys in plaintext. According to the source code in `tashfeenahmed/freellmapi`, the application implements a robust encryption layer that ensures confidentiality at rest while maintaining integrity through Galois/Counter Mode authentication tags. This design guarantees that sensitive keys remain cryptographically protected even if the underlying database is compromised.

## Master Encryption Key Initialization

The encryption subsystem centers on a single 32-byte master key that must be available before any cryptographic operations occur. In [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts), the `initEncryptionKey()` function handles secure key initialization at server startup.

### Production Configuration via ENCRYPTION_KEY

In production environments, the system expects a 64-character hexadecimal string (representing 32 bytes) in the `ENCRYPTION_KEY` environment variable. The code validates this format strictly; if the variable is missing or malformed, the server will fail to start, preventing accidental operation without encryption.

### Development Mode Key Generation

When `process.env.NODE_ENV !== 'production'`, FreeLLMAPI falls back to automatic key generation for local development. The logic:

1. Checks for an existing key in the `settings` SQLite table
2. Generates a fresh random 32-byte key using `crypto.randomBytes(32)` if none exists
3. Persists the new key to the database for consistency across restarts
4. Logs a warning that this auto-generated key should not be used in production

This fallback ensures developers can run the application immediately while maintaining the same encryption semantics as production.

## The AES-256-GCM Encryption Flow

When a user submits a new provider key via the API, FreeLLMAPI uses the `encrypt()` function exported from [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts) to secure the data before database insertion.

### Generating the Initialization Vector and Ciphertext

The `encrypt()` function creates a unique cryptographic context for every key:

```typescript
export function encrypt(text: string): { encrypted: string; iv: string; authTag: string } {
  const key = getEncryptionKey();               // 32-byte Buffer
  const iv = crypto.randomBytes(16);            // 16-byte IV
  const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);

  let encrypted = cipher.update(text, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  const authTag = cipher.getAuthTag().toString('hex'); // 16-byte tag

  return { encrypted, iv: iv.toString('hex'), authTag };
}

```

**Key implementation details:**
- **Unique IV**: Every encryption call generates a fresh 16-byte random initialization vector, ensuring that identical keys produce different ciphertexts
- **GCM Mode**: Uses Node.js `crypto.createCipheriv` with the `aes-256-gcm` algorithm, providing both confidentiality and authentication
- **Hex Encoding**: Returns all components as hexadecimal strings for safe storage in SQLite text columns

### Storing the Authentication Tag

The Galois/Counter Mode authentication tag is forced to 16 bytes via the internal `AUTH_TAG_BYTES` constant. This tag is essential for integrity verification during decryption, preventing undetected tampering with the encrypted payload.

## Decrypting API Keys for Outbound Requests

When the server needs to sign a request to an LLM provider, it reconstructs the original key using the `decrypt()` function in [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts).

### Integrity Verification with GCM Tags

The decryption process validates data integrity before returning the plaintext:

```typescript
export function decrypt(encrypted: string, iv: string, authTag: string): string {
  const key = getEncryptionKey();
  const decipher = crypto.createDecipheriv(
    'aes-256-gcm',
    key,
    Buffer.from(iv, 'hex'),
    { authTagLength: 16 }                      // pinned tag length
  );
  decipher.setAuthTag(Buffer.from(authTag, 'hex'));

  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  return decrypted;
}

```

**Security mechanisms:**
- **Explicit Tag Length**: The `authTagLength: 16` option prevents truncation attacks by requiring the full authentication tag
- **Verification**: `decipher.final()` automatically verifies the GCM tag; if the ciphertext, IV, or tag have been tampered with, Node.js throws an error and the request fails
- **Memory Safety**: The master key only exists in process memory during the decryption operation

## Database Storage and API Integration

The encryption layer integrates directly with the REST API routes defined in [`server/src/routes/keys.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/routes/keys.ts) and the underlying SQLite schema.

### Schema Design in api_keys Table

The database stores three distinct fields for each encrypted key:
- **`encrypted_key`**: The hex-encoded ciphertext
- **`iv`**: The 16-byte initialization vector used during encryption
- **`auth_tag`**: The 16-byte GCM authentication tag

This separation ensures that no single field contains sufficient information to recover the plaintext without the master key.

### Encrypting on Insert (POST /keys)

When creating a new key record, the route handler encrypts the raw provider key before persistence:

```typescript
const { encrypted, iv, authTag } = encrypt(keyToStore);
db.prepare(`
  INSERT INTO api_keys (platform, label, encrypted_key, iv, auth_tag, status, enabled)
  VALUES (?, ?, ?, ?, ?, 'unknown', 1)
`).run(platform, label ?? '', encrypted, iv, authTag);

```

This pattern ensures that raw keys never touch the database, even temporarily.

### Decrypting on Read and Usage

The system decrypts keys in two primary scenarios:

**1. Key Listing (GET /keys)**
For the administrative interface, the API decrypts keys to display masked versions using the `maskKey()` helper:

```typescript
const realKey = decrypt(row.encrypted_key, row.iv, row.auth_tag);
const maskedKey = maskKey(realKey); // Shows only first and last characters

```

**2. Outbound Provider Requests**
When routing requests to LLM providers (in services like [`media.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/media.ts) or [`embeddings.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/embeddings.ts)), the system retrieves the stored components and decrypts them to populate the `Authorization` header:

```typescript
const apiKey = decrypt(row.encrypted_key, row.iv, row.auth_tag);
const response = await fetch(providerEndpoint, {
  headers: { Authorization: `Bearer ${apiKey}` }
});

```

## Security Guarantees and Best Practices

FreeLLMAPI implements several defense-in-depth measures to protect provider credentials:

- **Confidentiality at Rest**: AES-256-GCM encryption ensures that database dumps or backups reveal no usable key material without the master `ENCRYPTION_KEY`
- **Integrity Protection**: The 16-byte GCM authentication tag prevents ciphertext tampering; any modification of the stored data causes immediate decryption failure
- **Minimal Exposure**: Only masked key representations (`maskKey()`) are ever sent to clients; full plaintext keys exist only ephemerally in server memory during outbound requests
- **Key Rotation Ready**: The master key is loaded once at startup, making rotation as simple as updating `ENCRYPTION_KEY` and re-encrypting the database contents

## Summary

- FreeLLMAPI uses **AES-256-GCM** via Node.js `crypto` module to encrypt all provider API keys before storage
- The system requires a **32-byte master key** (`ENCRYPTION_KEY`) in production, with automatic generation available only in development
- Each encryption operation generates a **unique 16-byte IV** and produces a **16-byte authentication tag**, both stored alongside the ciphertext
- Decryption occurs in [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts) using `decrypt()` and verifies integrity via the GCM tag before returning plaintext
- Raw keys are never persisted; only the encrypted triplet (ciphertext, IV, tag) exists in the `api_keys` table

## Frequently Asked Questions

### What happens if the ENCRYPTION_KEY environment variable is missing in production?

If `ENCRYPTION_KEY` is not set in a production environment, the `initEncryptionKey()` function in [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts) will throw an error during server startup. This fail-safe design prevents the application from running in an insecure state where it cannot encrypt or decrypt API keys.

### How does FreeLLMAPI handle key rotation?

FreeLLMAPI stores the master key in memory only, meaning you can rotate the encryption key by updating the `ENCRYPTION_KEY` environment variable and restarting the server. To re-encrypt existing keys with the new master key, you would need to iterate through the `api_keys` table, decrypt each entry with the old key, and re-encrypt with the new key.

### Is the initialization vector (IV) secret?

No, the IV does not need to remain secret. According to cryptographic best practices implemented in the codebase, the IV is stored plainly in the `iv` column and transmitted alongside the ciphertext. The IV ensures that encrypting the same key twice produces different outputs, preventing pattern analysis, but it is not a secret key component.

### What encryption algorithm does FreeLLMAPI use specifically?

FreeLLMAPI uses **AES-256-GCM** (Advanced Encryption Standard with 256-bit keys in Galois/Counter Mode), as specified in the `crypto.createCipheriv('aes-256-gcm', ...)` calls within [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts). This mode provides both authenticated encryption (confidentiality) and integrity verification (tamper detection) in a single operation.