# How FreeLLMAPI Implements API Key Encryption: AES-256-GCM Security Deep Dive

> Learn how FreeLLMAPI secures provider API keys with AES-256-GCM encryption. Discover its robust key management and secure storage in SQLite, ensuring client data protection.

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

---

**FreeLLMAPI encrypts all provider API keys using AES-256-GCM with a 64-character hex encryption key stored in the `ENCRYPTION_KEY` environment variable, persisting the ciphertext, 16-byte IV, and 16-byte auth tag in SQLite while never exposing plaintext keys to clients.**

FreeLLMAPI is an open-source LLM gateway that aggregates multiple AI providers behind a unified API. Because the service stores sensitive third-party credentials, it implements **API key encryption** using Node.js's native `crypto` module. The entire cryptographic workflow is isolated in [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts) and employs authenticated encryption to ensure secrets remain protected at rest.

## Encryption Architecture and Key Initialization

The security model begins with master key management in [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts). This module handles both key derivation and cryptographic operations through two distinct modes.

**Production key sourcing**: In production environments, FreeLLMAPI expects a hex-encoded 64-character (32-byte) key supplied via the `ENCRYPTION_KEY` environment variable. The `initEncryptionKey()` function validates this using `parseHexKey()`, which enforces exact byte length and hexadecimal format to prevent weak or malformed configurations.

**Development fallback**: When `NODE_ENV` is not production and no valid key is detected, the system auto-generates a cryptographically random key, persists it to the local `settings` table, and reuses it for the lifetime of that database instance. This ensures zero-configuration local development while making it obvious that production deployments require explicit key management.

```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');
    return;
  }
  // dev fallback: read from DB or generate new key
  // ...
}

```

## The Encryption Process (AES-256-GCM)

When users add new provider keys via `POST /keys` or `POST /keys/custom`, the plaintext never touches the database directly. Instead, the `encrypt()` function performs authenticated encryption:

```typescript
// server/src/lib/crypto.ts
export function encrypt(text: string): { encrypted: string; iv: string; authTag: string } {
  const key = getEncryptionKey();
  const iv = crypto.randomBytes(16);
  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 implementation uses:
- **Algorithm**: `aes-256-gcm` for authenticated encryption with associated data (AEAD)
- **IV**: 16 cryptographically random bytes (128 bits) generated via `crypto.randomBytes(16)`
- **Auth Tag**: 16 bytes (`AUTH_TAG_BYTES = 16`) to prevent truncation attacks

The resulting three components—`encrypted`, `iv`, and `authTag`—are stored in the `api_keys` table in their respective columns (`encrypted_key`, `iv`, `auth_tag`).

## The Decryption Workflow

Whenever backend services need the real provider credentials—such as in [`server/src/services/router.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/router.ts), [`media.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/media.ts), [`embeddings.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/embeddings.ts), or [`health.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/health.ts)—they retrieve the stored components and invoke `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;
}

```

The decryption process validates integrity through GCM's built-in authentication. If the authentication tag verification fails—indicating tampering, corruption, or key mismatch—the operation throws an error. Routes in [`server/src/routes/keys.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/routes/keys.ts) catch these failures and return `"[decrypt failed]"` to prevent information leakage while maintaining API stability.

## Security Hardening and Mitigations

FreeLLMAPI implements several defense-in-depth measures within [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts):

- **Strict key validation**: `parseHexKey()` rejects any key not exactly 32 bytes (64 hex characters) and aborts startup if the format is invalid.
- **Placeholder protection**: The code explicitly refuses to use the placeholder string `your-64-char-hex-key-here` in production environments.
- **Truncation attack prevention**: Setting `AUTH_TAG_BYTES = 16` forces full-size authentication tags, closing vulnerability windows where truncated tags might be accepted by older OpenSSL versions.
- **Logging safety**: Only masked keys (via `maskKey()`) ever leave the server process; plaintext keys are never serialized to logs or HTTP responses.

## End-to-End Key Lifecycle

The complete flow spans multiple files across the codebase:

1. **Startup**: `initEncryptionKey()` runs immediately after database initialization in [`server/src/app.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/app.ts).
2. **Storage**: Routes in [`server/src/routes/keys.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/routes/keys.ts) encrypt incoming plaintext before executing INSERT operations against the `api_keys` table.
3. **Retrieval**: Service layers decrypt on-demand when routing requests to upstream providers, keeping plaintext only in process memory.
4. **Rotation**: Updates trigger re-encryption with fresh IVs; deletions permanently remove the encrypted material from the database.

By isolating all cryptographic logic in [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts), the codebase remains auditable and supports straightforward algorithm replacement if future security requirements demand it.

## Summary

- FreeLLMAPI uses **AES-256-GCM** authenticated encryption via Node.js `crypto` for all API key storage at rest.
- Production requires a 64-character hex `ENCRYPTION_KEY` environment variable; development auto-generates ephemeral keys persisted to SQLite.
- Encryption produces three stored components per key: ciphertext, 16-byte IV, and 16-byte auth tag in the `api_keys` table columns.
- Decryption occurs only in backend service layers ([`router.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/router.ts), [`media.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/media.ts), [`embeddings.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/embeddings.ts), [`health.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/health.ts)) and never exposes plaintext to API clients.
- Security controls prevent weak keys, placeholder usage in production, truncation attacks, and accidental credential logging.

## Frequently Asked Questions

### What encryption algorithm does FreeLLMAPI use?

FreeLLMAPI uses **AES-256-GCM** (Galois/Counter Mode) as implemented in Node.js's `crypto` module. This authenticated encryption mode provides both confidentiality and integrity verification through a 16-byte authentication tag generated during encryption and validated during decryption in [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts).

### How is the master encryption key managed?

In production, FreeLLMAPI requires the `ENCRYPTION_KEY` environment variable set to a 64-character hexadecimal string representing 32 bytes of entropy. The `initEncryptionKey()` function validates this strictly using `parseHexKey()` and refuses to start if the placeholder value is detected. During development, the system automatically generates a random key, stores it in the local SQLite `settings` table, and reuses it until the database is reset.

### Can encrypted API keys be leaked through the REST API?

No. The [`server/src/routes/keys.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/routes/keys.ts) endpoints decrypt stored credentials only to pass them through `maskKey()`, which returns truncated representations (such as `sk-...7890`) to clients. The plaintext key exists only briefly in memory during active decryption within backend service layers, and the AES-GCM authentication tag prevents undetected tampering with stored ciphertext.

### What happens if decryption fails?

If `decrypt()` encounters corrupted data, an invalid authentication tag, or a mismatched encryption key, the function throws an error that calling code catches. For example, when listing keys in [`server/src/routes/keys.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/routes/keys.ts), decryption failures result in the placeholder string `"[decrypt failed]"` being returned instead of crashing the server or exposing stack traces that could reveal implementation details.