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

> FreeLLMAPI securely stores and encrypts API keys using AES-256-GCM. Learn how this robust encryption protects your credentials at rest and in memory for enhanced security.

- Repository: [Tashfeen/freellmapi](https://github.com/tashfeenahmed/freellmapi)
- Tags: internals
- Published: 2026-08-28

---

**FreeLLMAPI encrypts every provider API key using AES-256-GCM with a 32-byte encryption key stored separately from the database, ensuring credentials remain encrypted at rest and are only decrypted in memory during active requests.**

The FreeLLMAPI repository implements a comprehensive encryption scheme for protecting sensitive provider credentials. Unlike simple hashing or base64 encoding, the project uses industry-standard **AES-256-GCM authenticated encryption** with unique initialization vectors per key, ensuring both confidentiality and integrity of stored API keys.

## Encryption Architecture Overview

FreeLLMAPI stores all provider credentials—including API keys and custom proxy URLs—in an **SQLite database with encrypted values only**. The plaintext never persists to disk at any point.

The encryption system centers on three core design principles:

- **Separation of encryption key and data**: The `ENCRYPTION_KEY` lives outside the database entirely
- **Authenticated encryption**: GCM mode provides tamper detection via authentication tags
- **Ephemeral decryption**: Keys exist in plaintext only for the duration of a single request

## Core Encryption Module: [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts)

The [`crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/crypto.ts) file contains the entire encryption implementation. Here is the actual encryption flow used when storing a new API key:

```typescript
// Encrypt a raw provider key before persisting
import { encrypt } from './server/src/lib/crypto.js';

const rawKey = 'sk-abc123…';
const { encrypted, iv, authTag } = encrypt(rawKey);

// Store in SQLite (pseudo-code)
db.prepare(`
  INSERT INTO api_keys (platform, label, encrypted_key, iv, auth_tag)
  VALUES (?, ?, ?, ?, ?)
`).run('openai', 'my-key', encrypted, iv, authTag);

```

The `encrypt()` function returns an object with three components:

1. **`encrypted`** – The ciphertext in hexadecimal format
2. **`iv`** – A 16-byte initialization vector (hex-encoded), unique per encryption operation
3. **`authTag`** – The GCM authentication tag (hex-encoded) for integrity verification

### Decryption for Active Requests

When a request requires a provider key, the system retrieves the encrypted components and decrypts them in memory:

```typescript
// Decrypt a stored key for an outgoing request
import { decrypt } from './server/src/lib/crypto.js';

const row = db.prepare(`
  SELECT encrypted_key, iv, auth_tag FROM api_keys WHERE id = ?
`).get(keyId);

const plaintextKey = decrypt(row.encrypted_key, row.iv, row.auth_tag);
// Use `plaintextKey` as the Authorization header

```

The plaintext key exists only in memory for the duration of the request and is never logged or cached.

## Encryption Key Management

The **32-byte encryption key** (hex-encoded as 64 characters) is sourced through a tiered fallback mechanism:

1. **Primary**: Environment variable `ENCRYPTION_KEY`
2. **Development fallback**: Auto-generated random key stored in `.encryption-key` file

### Development Key Generation

When `ENCRYPTION_KEY` is unset, the server creates a `.encryption-key` file with strict permissions:

- File mode `0600` (owner read/write only)
- Placement adjacent to the SQLite database file
- Ownership restricted to the process user

This key file **never contains database data**—it only holds the encryption key itself, maintaining proper separation.

### Key Fingerprinting for Verification

For operational diagnostics without key exposure, the crypto module provides `encryptionKeyFingerprint()`:

```typescript
// Returns SHA-256 hash of the encryption key
const fingerprint = encryptionKeyFingerprint();

```

This allows administrators to verify that a database backup or migration uses the expected encryption key without revealing the key itself.

## Database Schema and Storage

The `api_keys` table schema stores encrypted components in separate columns, as defined in the migration files under `server/src/db/migrations/`:

| Column | Purpose |
|--------|---------|
| `encrypted_key` | AES-256-GCM ciphertext (hex) |
| `iv` | 16-byte initialization vector (hex) |
| `auth_tag` | GCM authentication tag (hex) |

Additional columns `proxy_encrypted`, `proxy_iv`, and `proxy_auth_tag` store encrypted custom proxy URLs using the same pattern.

## API Route Implementation: [`server/src/routes/keys.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/routes/keys.ts)

The `/keys` endpoint handles incoming key submissions. According to the source code in [`server/src/routes/keys.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/routes/keys.ts), the route:

1. Receives the plaintext key from UI or API client
2. Calls `encrypt(keyValue)` from the crypto module
3. Inserts the three encrypted components into `api_keys`

No plaintext key ever reaches the database layer.

## Key Masking for UI Safety

To prevent accidental exposure in administrative interfaces, `maskKey()` in [`crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/crypto.ts) generates display-safe versions:

```typescript
// Mask a key for UI display
import { maskKey } from './server/src/lib/crypto.js';

const masked = maskKey('sk-abc123…'); // → "sk-a…c123"

```

The masking logic shows:
- First 4 and last 4 characters for keys longer than 12 characters
- Generic `"****"` for shorter keys

The UI **never receives real keys**—only masked representations or encrypted storage operations.

## Proxy URL Encryption: [`server/src/lib/key-proxy.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/key-proxy.ts)

Custom proxy URLs receive identical protection via `encryptProxyUrl()` and `decryptProxyUrl()` in [`server/src/lib/key-proxy.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/key-proxy.ts). These functions wrap the same AES-256-GCM primitives, storing results in the `proxy_*` column variants.

## Security Documentation References

The repository includes dedicated security documentation:

- [`docs/architecture.md`](https://github.com/tashfeenahmed/freellmapi/blob/main/docs/architecture.md) – High-level description of SQLite + AES-256-GCM storage
- [`SECURITY.md`](https://github.com/tashfeenahmed/freellmapi/blob/main/SECURITY.md) – Operational security considerations for key handling

## Summary

- **Algorithm**: AES-256-GCM authenticated encryption with unique IVs per key
- **Key storage**: Environment variable `ENCRYPTION_KEY` or `.encryption-key` file with mode 0600
- **Database storage**: Three-column schema (`encrypted_key`, `iv`, `auth_tag`) in SQLite
- **Runtime behavior**: Decryption occurs in memory only, for the duration of each request
- **UI protection**: `maskKey()` prevents accidental exposure in interfaces
- **Proxy support**: Identical encryption flow via [`server/src/lib/key-proxy.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/key-proxy.ts)

FreeLLMAPI's encryption implementation follows cryptography best practices by separating keys from data, using authenticated encryption, and minimizing plaintext exposure to absolute runtime necessity.

## Frequently Asked Questions

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

FreeLLMAPI uses **AES-256-GCM**, an authenticated encryption mode that provides both confidentiality and integrity verification. The implementation requires a 32-byte key hex-encoded as 64 characters, with unique 16-byte initialization vectors generated for each encryption operation.

### Where is the encryption key stored in FreeLLMAPI?

The encryption key is **never stored in the database**. It is sourced from the `ENCRYPTION_KEY` environment variable, or auto-generated into a `.encryption-key` file with 0600 permissions in development environments. This separation ensures that database compromise alone cannot decrypt stored credentials.

### How does FreeLLMAPI prevent API keys from appearing in logs or UI?

FreeLLMAPI uses the `maskKey()` function to generate display-safe versions showing only first/last characters. The actual `decrypt()` operation occurs only when building outbound provider requests, and plaintext keys exist in memory **only for the duration of that specific request cycle**.

### Can proxy URLs be encrypted the same way as API keys?

Yes. The [`server/src/lib/key-proxy.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/key-proxy.ts) module provides `encryptProxyUrl()` and `decryptProxyUrl()` functions that use identical AES-256-GCM encryption, storing results in dedicated columns (`proxy_encrypted`, `proxy_iv`, `proxy_auth_tag`) within the same `api_keys` table.