# How AES-256-GCM Encryption Works for Stored API Keys in FreeLLMAPI

> Learn how FreeLLMAPI secures API keys with AES-256-GCM encryption. Discover how it uses random IVs and authentication tags to protect your data.

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

---

**FreeLLMAPI uses AES-256-GCM authenticated encryption to ensure provider API keys are never stored in plain text, generating a random 16-byte IV for each encryption operation and verifying integrity via a pinned 16-byte authentication tag during decryption.**

FreeLLMAPI is an open-source LLM routing gateway that protects sensitive third-party credentials using industry-standard symmetric encryption. When administrators add provider API keys through the dashboard, the system immediately encrypts them using a 256-bit key before persisting to the SQLite database. This implementation, located in [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts), ensures that credentials remain confidential and tamper-evident while at rest.

## Initializing the 256-Bit Encryption Key

On server startup, the `initEncryptionKey()` function validates and loads the master encryption key. In production deployments, you must supply a 64-character hex string via the `ENCRYPTION_KEY` environment variable, which the system decodes to exactly 32 bytes (256 bits) as defined by the constant `KEY_BYTES = 32`.

The validation logic in lines 8-30 ensures the key meets the `KEY_HEX_LEN = 64` requirement, rejecting malformed inputs. A placeholder check in lines 54-58 prevents accidental deployment with default or example credentials. For local development, the system auto-generates a cryptographically secure fallback key, persists it in the SQLite `settings` table, and reuses it across restarts to maintain data accessibility without requiring manual configuration.

## Encrypting API Keys with AES-256-GCM

When a new provider key is saved through the dashboard, the `encrypt()` function executes a four-step authenticated encryption process. First, it generates a fresh 16-byte initialization vector (IV) using `crypto.randomBytes(16)` to ensure uniqueness across encryptions. It then initializes a cipher via `crypto.createCipheriv('aes-256-gcm', key, iv)` and processes the plaintext API key.

After encryption completes, the function extracts the 16-byte authentication tag using `cipher.getAuthTag()`. This tag acts as a cryptographic checksum. The function returns an object containing the hex-encoded ciphertext, IV, and auth tag, which are then stored in the `api_keys` table. This implementation appears in lines 85-99 of [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts).

```typescript
import { encrypt } from '@/lib/crypto';

// Encrypt a provider API key before database storage
const plain = 'sk-my-google-key';
const { encrypted, iv, authTag } = encrypt(plain);

// Store the three hex strings in the database
db.prepare(`
  INSERT INTO api_keys (provider, encrypted_key, iv, auth_tag)
  VALUES (?, ?, ?, ?)
`).run('google', encrypted, iv, authTag);

```

## Decrypting Keys and Verifying Integrity

To retrieve a stored key for an outbound request, the `decrypt()` function reconstructs the decipher using `crypto.createDecipheriv('aes-256-gcm', key, iv, { authTagLength: 16 })`. It explicitly sets the authentication tag with `decipher.setAuthTag()`, converting the stored hex string back to bytes.

During decryption, OpenSSL automatically verifies that the authentication tag matches the ciphertext. If any bit of the encrypted data, IV, or auth tag has been modified, the decryption throws an error before returning plaintext. This integrity verification, implemented in lines 107-115, ensures that compromised database files cannot be silently altered to inject malicious keys.

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

// Retrieve and decrypt a stored provider key
const row = db
  .prepare('SELECT encrypted_key, iv, auth_tag FROM api_keys WHERE provider = ?')
  .get('google');

const plainKey = decrypt(row.encrypted_key, row.iv, row.auth_tag);
// plainKey now contains the original API credential

```

## Security Hardening Against Shortened-Tag Attacks

The implementation explicitly pins the GCM authentication tag length to **16 bytes** (`AUTH_TAG_BYTES = 16`) as shown in lines 101-105. This mitigates the shortened-tag attack vulnerability that Node.js allows by default, where the crypto module accepts tags between 4 and 16 bytes. By enforcing the full 16-byte tag through the `authTagLength` option in `createDecipheriv`, FreeLLMAPI eliminates the risk of authentication bypass through tag truncation.

```typescript
import Database from 'better-sqlite3';
import { initEncryptionKey } from '@/lib/crypto';

// Initialize encryption subsystem on server startup
const db = new Database('data/freellmapi.db');
initEncryptionKey(db);  // Loads ENCRYPTION_KEY from env or generates dev fallback

```

## Summary

- **AES-256-GCM** provides both confidentiality and integrity, ensuring API keys cannot be read or modified without detection.
- **Key management** requires a 32-byte (64-character hex) `ENCRYPTION_KEY` in production, with automatic fallback generation for development environments.
- **Per-key randomization** uses a unique 16-byte IV for every encryption operation, preventing pattern analysis.
- **Tamper detection** relies on a pinned 16-byte authentication tag; any modification to stored data triggers a decryption failure.
- **Source protection** keeps the master key in environment variables or encrypted SQLite rows, never in source control.

## Frequently Asked Questions

### What is AES-256-GCM and why does FreeLLMAPI use it?

AES-256-GCM (Advanced Encryption Standard with Galois/Counter Mode) is a symmetric authenticated encryption algorithm that provides both data confidentiality and integrity verification. FreeLLMAPI uses it because GCM mode generates an authentication tag during encryption that binds the ciphertext to its IV, allowing the system to detect any unauthorized modifications to stored API keys while maintaining high performance.

### How is the encryption key managed in production environments?

In production, the encryption key must be supplied via the `ENCRYPTION_KEY` environment variable as a 64-character hexadecimal string representing 32 bytes of entropy. The `initEncryptionKey()` function in [`server/src/lib/crypto.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/lib/crypto.ts) validates this key on startup and rejects any keys that do not match the exact length requirements, preventing runtime errors during cryptographic operations.

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

If `ENCRYPTION_KEY` is not set, the system enters development mode and automatically generates a secure random key using `crypto.randomBytes(32)`. This key is then persisted in the SQLite `settings` table and reused across server restarts. While this ensures functionality for local development, production deployments will fail to start if the environment variable is missing or contains a placeholder value, as enforced by the checks in lines 54-58.

### How does FreeLLMAPI detect tampering with stored API keys?

The `decrypt()` function detects tampering through the GCM authentication tag. When decrypting, it sets the expected auth tag using `decipher.setAuthTag()`. If the stored ciphertext, IV, or auth tag has been altered, the authentication check fails and Node.js throws an error before returning the plaintext. This ensures that attackers cannot modify encrypted keys to redirect requests or extract credentials without immediate detection.