# How Encrypted Key Storage with AES-256-GCM Works in FreeLLMAPI

> Learn how FreeLLMAPI secures API keys with AES-256-GCM encryption. Discover the split storage of ciphertext, IV, and auth tag for maximum confidentiality at rest.

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

---

**FreeLLMAPI stores all provider API keys using AES-256-GCM encryption, splitting each secret into ciphertext, IV, and authentication tag across three separate database columns to ensure confidentiality at rest.**

FreeLLMAPI is an open‑source LLM proxy router that must safely persist sensitive provider credentials. Rather than storing raw keys, the codebase implements a hardened encryption layer in [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts) that leverages Node.js native crypto with AES‑256‑GCM. This design guarantees that even if the SQLite database file is exfiltrated, the keys remain inaccessible without the 32‑byte master key.

## Master Key Initialization

The encryption lifecycle begins with `initEncryptionKey()`, defined in **[[`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts)](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts)**. This function resolves the master key through a strict hierarchy:

1. **Production Environment** – When the `ENCRYPTION_KEY` environment variable is present, the code validates it as exactly 64 hex characters (32 bytes) via `parseHexKey()` and loads it directly into memory.
2. **Development Fallback** – In non‑production builds, if the env var is missing, the system looks for a file named `.encryption-key` adjacent to the SQLite database. If absent, it generates a fresh random key, writes it atomically to that file, and caches it for the process lifetime.
3. **Legacy/Test Path** – For in‑memory or legacy databases, the key may be read from the deprecated `settings` table, though this is reserved strictly for test suites.

Once loaded, the master key never touches disk again (unless the dev‑fallback file is created during first run). `encryptionKeyFingerprint()` derives a public SHA‑256 identifier (first 16 hex chars) from the key for logging and diagnostics without revealing the secret itself.

## The Encryption Flow

When a user saves a new provider key, the `encrypt(text)` function (also in **[[`crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/crypto.ts)](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts)**) executes the following steps:

- Generates a cryptographically random 16‑byte **initialization vector (IV)** using `crypto.randomBytes(16)`.
- Creates a `Cipher` instance via `crypto.createCipheriv('aes-256-gcm', key, iv)`.
- Streams the plaintext through the cipher to produce hex‑encoded ciphertext.
- Extracts the 16‑byte **authentication tag** from `cipher.getAuthTag()`.

The function returns an object containing `{ encrypted, iv, authTag }`, which the persistence layer then writes to the `api_keys` table.

## Database Storage Schema

The `api_keys` table stores encrypted secrets in three distinct columns to prevent concatenation attacks and enable strict validation:

- `key_encrypted` – The hex‑encoded AES‑256‑GCM ciphertext.
- `key_iv` – The 16‑byte IV used for that specific encryption operation.
- `key_auth_tag` – The 16‑byte GCM authentication tag.

The same triplet pattern applies to optional per‑key proxy URLs (`proxy_encrypted`, `proxy_iv`, `proxy_auth_tag`) handled in **[[`server/src/lib/key-proxy.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/key-proxy.ts)](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/key-proxy.ts)**. This columnar separation ensures that an attacker who can modify only one field will trigger an integrity failure during decryption.

## Decryption and Verification

When the router needs a credential for an outgoing request, it invokes `decrypt(encrypted, iv, authTag)` from **[[`crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/crypto.ts)](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts)**:

1. Reconstructs a `Decipher` using the master key, provided IV, and explicitly sets `authTagLength: 16`.
2. Calls `decipher.setAuthTag()` with the stored 16‑byte tag before any data is decrypted.
3. Returns the original plaintext; if the tag verification fails (indicating tampering or a mismatched master key), the operation throws.

All decryption calls in **[[`server/src/services/router.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/router.ts)](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/router.ts)** are wrapped in try/catch blocks. If a row cannot be decrypted—typically because the database was migrated to a host with a different `ENCRYPTION_KEY`—the router gracefully falls back to the global proxy configuration rather than crashing.

## Proxy URL Encryption and Masking

Per‑key proxy credentials receive identical protection via **[[`key-proxy.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/key-proxy.ts)](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/key-proxy.ts)**. The module exports:

- `encryptProxyUrl()` – Wraps the URL string through the same AES‑256‑GCM pipeline.
- `decryptProxyUrl()` – Reverses the process; returns an empty string on failure to avoid leaking stack traces.
- `maskProxyUrl()` – Displays safe previews such as `socks5://alice:***@proxy.internal:1080` in the UI, ensuring the plaintext secret is never echoed to the browser.

`maskKey()` provides the same redaction service for API keys, showing only the last four characters in administrative views.

## Summary

- **AES‑256‑GCM** provides authenticated encryption for all provider credentials in FreeLLMAPI.
- The master key is sourced from the `ENCRYPTION_KEY` environment variable or a `.encryption-key` dev‑fallback file, validated as 64 hex characters.
- Ciphertext, IV, and auth tags are stored in separate SQLite columns (`key_encrypted`, `key_iv`, `key_auth_tag`).
- Decryption enforces a fixed 16‑byte auth tag length to prevent truncation attacks.
- Failures during decryption are handled gracefully, falling back to global settings rather than exposing errors.
- UI masking utilities prevent accidental disclosure of keys in logs or frontend interfaces.

## Frequently Asked Questions

### What happens if I lose the ENCRYPTION_KEY environment variable?

If the master key is lost, all previously encrypted rows become permanently undecryptable because the system relies solely on that 32‑byte key for AES‑256‑GCM decryption. The application will continue to run but will treat stored keys as unusable, falling back to global proxy settings or prompting for re‑entry. Always back up the `ENCRYPTION_KEY` or the `.encryption-key` file created during development.

### Why does FreeLLMAPI use AES‑256‑GCM instead of AES-256-CBC?

**AES‑256‑GCM** provides both confidentiality and authenticity, whereas CBC mode only offers confidentiality. The 16‑byte authentication tag generated during encryption ensures that any tampering with the ciphertext or IV is detected during decryption. Additionally, GCM is a streaming mode that works efficiently with Node.js `createCipheriv`, eliminating the need for manual padding schemes.

### How is the master key protected during development?

In non‑production builds, `initEncryptionKey()` generates a random 32‑byte key if one does not exist and writes it atomically to a `.encryption-key` file next to the database. This file has restrictive permissions (when the OS supports it) and is loaded into memory once at startup. The key is never logged; only a SHA‑256 fingerprint is displayed in diagnostics.

### Can I rotate the encryption key without losing data?

The codebase does not currently implement automatic re‑encryption. To rotate the master key, you must decrypt all existing rows using the old key, update the `ENCRYPTION_KEY` environment variable, and re‑insert the plaintext values so they are encrypted with the new master key. Because the decryption flow in [`router.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/router.ts) falls back gracefully on failure, you can perform this migration incrementally without downtime.