# How API Key Encryption Works in FreeLLMAPI: AES-256-GCM Implementation Guide

> FreeLLMAPI encrypts API keys with AES-256-GCM using Node.js crypto. Learn how this implementation stores ciphertext, IV, and auth tag in SQLite for secure API key management.

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

---

**FreeLLMAPI encrypts provider API keys using AES-256-GCM via Node.js crypto, storing the ciphertext, IV, and authentication tag in SQLite while deriving the master key from the ENCRYPTION_KEY environment variable.**

The tashfeenahmed/freellmapi project implements robust API key encryption to protect sensitive provider credentials at rest. Written in TypeScript for Node.js, the system isolates all cryptographic operations in [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts), ensuring that raw API keys never persist in plaintext and are only decrypted transiently in memory when routing requests to LLM providers.

## Core Encryption Architecture

FreeLLMAPI uses symmetric encryption with the Node.js native `crypto` module. The design prioritizes authenticated encryption and strict key validation to prevent common implementation flaws.

### Encryption Algorithm and Mode

The system employs **AES-256-GCM** (Advanced Encryption Standard with Galois/Counter Mode). This provides both confidentiality and authenticity, ensuring that tampered ciphertext is detected during decryption. The algorithm identifier is defined as `aes-256-gcm` in the source code.

### Master Key Initialization

The encryption key originates from one of two sources:

- **Production**: A hex-encoded 64-character string (32 bytes) provided via the `ENCRYPTION_KEY` environment variable
- **Development**: An auto-generated key persisted in the local SQLite `settings` table when `NODE_ENV !== 'production'`

In [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts), the `initEncryptionKey()` function validates the environment variable using `parseHexKey()`, which enforces exact length and hex formatting. The code explicitly rejects the placeholder value `your-64-char-hex-key-here` in production environments to prevent accidental deployment with dummy credentials.

## Encryption Workflow in crypto.ts

All cryptographic operations are centralized in [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts), exposing three primary functions: `initEncryptionKey()`, `encrypt()`, and `decrypt()`.

### The encrypt() Function

When a user adds a new provider key via `POST /keys`, the plaintext is passed to 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 a 16-byte random initialization vector (IV) and returns an object containing the hex-encoded ciphertext, IV, and 16-byte authentication tag. The constant `AUTH_TAG_BYTES` is set to `16` to prevent truncation attacks.

### The decrypt() Function

Service layers retrieve stored keys using the `decrypt()` function, which requires the three components returned by `encrypt()`:

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

```

If decryption fails due to corruption or an invalid key, the calling code handles the error gracefully, often falling back to `"[decrypt failed]"` to prevent application crashes.

## Database Storage Schema

Encrypted keys are stored in the SQLite `api_keys` table with the following schema relevant to encryption:

- `encrypted_key` (TEXT): The hex-encoded AES-256-GCM ciphertext
- `iv` (TEXT): The hex-encoded 16-byte initialization vector
- `auth_tag` (TEXT): The hex-encoded 16-byte GCM authentication tag

This separation ensures that no single column contains sufficient information to reconstruct the plaintext without access to the master encryption key.

## Usage Patterns Across the Codebase

The encryption layer is consumed by both the HTTP API routes and internal service layers.

### Route Layer Implementation

In [`server/src/routes/keys.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/routes/keys.ts), the creation endpoint encrypts before persistence:

```typescript
// During POST /keys
const { encrypted, iv, authTag } = encrypt(apiKey);
// INSERT into api_keys (encrypted_key, iv, auth_tag, ...)

```

When listing keys, the route decrypts each entry and masks the result:

```typescript
// server/src/routes/keys.ts
const realKey = decrypt(row.encrypted_key, row.iv, row.auth_tag);
const maskedKey = maskKey(realKey); // Returns "sk-...abcd" format
// Only maskedKey is sent to the client

```

### Service Layer Decryption

Internal services decrypt keys transiently for provider SDK calls. For example, in [`server/src/services/router.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/router.ts) (around line 585), the media service, embeddings service, and health check service all follow this pattern:

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

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

```

The plaintext key exists only in memory during the request lifecycle and is never logged or cached.

## Security Considerations and Mitigations

FreeLLMAPI implements several defenses against common cryptographic failures:

- **Strict key validation**: The `parseHexKey()` function enforces exactly 64 hex characters (32 bytes), aborting on malformed keys.
- **Placeholder protection**: The code compares against `PLACEHOLDER_KEY` and refuses to use the default dummy value in production.
- **Auth tag enforcement**: Setting `AUTH_TAG_BYTES = 16` prevents truncation attacks that could reduce authentication strength.
- **Memory-only plaintext**: Only masked keys (via `maskKey()`) leave the server process; decrypted values are garbage-collected after use.
- **Dev isolation**: Auto-generated development keys are stored in the local database file, ensuring they cannot be mistakenly deployed.

## Summary

- **Algorithm**: AES-256-GCM authenticated encryption using Node.js `crypto`
- **Key source**: 64-character hex `ENCRYPTION_KEY` environment variable (32 bytes decoded)
- **Storage**: Separate `encrypted_key`, `iv`, and `auth_tag` columns in SQLite `api_keys` table
- **Implementation**: Centralized in [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts) with `encrypt()` and `decrypt()` functions
- **Security**: Enforces full 16-byte auth tags, rejects placeholder keys, and only exposes masked key representations via the API

## Frequently Asked Questions

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

FreeLLMAPI uses **AES-256-GCM** (Advanced Encryption Standard in Galois/Counter Mode) as implemented in [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts). This provides authenticated encryption, ensuring both confidentiality and integrity of the stored API keys.

### How long must the ENCRYPTION_KEY be?

The `ENCRYPTION_KEY` environment variable must contain exactly **64 hexadecimal characters**, which decode to 32 bytes (256 bits). The `parseHexKey()` validation function rejects keys of any other length or format.

### Where are the encrypted API keys stored?

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

### What happens if decryption fails?

If the `decrypt()` function encounters invalid data or an incorrect master key, it throws an error that calling code typically catches. For example, in [`server/src/routes/keys.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/routes/keys.ts), failed decryptions result in a masked key value of `"[decrypt failed]"` being returned to the client, preventing application crashes while alerting administrators to data integrity issues.