# How OmniRoute Protects API Keys with AES-256-GCM Encryption

> Discover how OmniRoute secures API keys using AES-256-GCM encryption and scrypt-derived keys. Learn about field-level protection and legacy data migration.

- Repository: [Diego Rodrigues de Sa e Souza/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- Tags: how-to-guide
- Published: 2026-08-13

---

**TL;DR:** OmniRoute uses field-level AES-256-GCM encryption with scrypt-derived keys to protect API keys, storing them in a prefixed format that includes IV, ciphertext, and authentication tag, with automatic migration for legacy data.

OmniRoute is an open-source API routing platform that handles sensitive credentials including API keys and access tokens. The project implements a comprehensive encryption layer in [[`src/lib/db/encryption.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/encryption.ts)](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/db/encryption.ts) to ensure these secrets are never persisted in plaintext. This article examines how OmniRoute protects API keys with AES-256-GCM encryption, covering the cryptographic implementation, key derivation strategy, and migration handling.

## AES-256-GCM Implementation Details

OmniRoute's encryption module uses Node.js's native `crypto` module to implement **authenticated encryption** via AES-256-GCM. This cipher mode provides both confidentiality and integrity protection in a single operation.

### Encryption Format and Structure

Encrypted values follow a strict string format that embeds all necessary components for decryption:

```

enc:v1:<iv_hex>:<ciphertext_hex>:<authTag_hex>

```

This format appears in [[`src/lib/db/encryption.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/encryption.ts) lines 30-41](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/db/encryption.ts#L30-L41). The `v1` prefix enables future algorithm versioning, while the hexadecimal encoding ensures safe storage in SQLite text columns.

### Authentication Tag Enforcement

GCM's security depends on proper authentication tag handling. OmniRoute explicitly sets `authTagLength: 16` in the `createDecipheriv` options, rejecting any ciphertext with truncated tags. This mitigates **tag truncation attacks** where an attacker might forge valid-looking ciphertext by manipulating shorter tags, as implemented in [lines 34-38](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/db/encryption.ts#L34-L38).

## Key Derivation with scryptSync

The encryption key is derived from the `STORAGE_ENCRYPTION_KEY` environment variable using **scrypt**, a memory-hard key derivation function resistant to hardware acceleration attacks.

### Static Salt for Deterministic Keys

Since version 3.7.9, OmniRoute uses a **static salt** (`"omniroute-field-encryption-v1"`) for all key derivations. This design choice, found in [lines 41-53](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/db/encryption.ts#L41-L53) and [lines 84-92](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/db/encryption.ts#L84-L92), ensures that:

- The same deployment configuration produces identical key material
- Encrypted values remain portable across database backups
- No salt storage is required alongside ciphertext

The static salt is combined with scrypt's default parameters (N=16384, r=8, p=1) to derive a 32-byte key suitable for AES-256.

### Legacy Dynamic Salt Support

For backward compatibility, the module retains support for **dynamic per-row salts** used in earlier versions. The `decrypt` function attempts static-salt decryption first, then falls back to legacy derivation if needed, as shown in [lines 103-112](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/db/encryption.ts#L103-L112).

## Encryption API and Usage Patterns

The module exports three core functions for protecting API keys and credentials.

### `encrypt(value)` — Field-Level Encryption

The `encrypt` function generates a fresh 16-byte IV for each operation, ensuring **semantic security** (identical plaintexts produce distinct ciphertexts). The implementation in [lines 44-66](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/db/encryption.ts#L44-L66):

```typescript
import { encrypt, decrypt } from '@/src/lib/db/encryption';

// Encrypt a single API key
const rawKey = 'sk-my-super-secret-key';
const cipher = encrypt(rawKey);
// Result: "enc:v1:9f2b...:a1b2c3...:d4e5f6..."

```

### `decrypt(ciphertext, options)` — Secure Retrieval

Decryption enforces integrity verification through GCM's authentication tag. Failure modes are handled explicitly in [lines 85-104](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/db/encryption.ts#L85-L104) and [lines 126-138](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/db/encryption.ts#L126-L138):

- Authentication failures return `null` rather than throwing
- The `quiet` option suppresses error logging for expected failures
- Legacy format detection triggers automatic re-encryption

```typescript
const recovered = decrypt(cipher);
console.log(recovered === rawKey); // true or null on failure

```

### Connection Object Helpers

For structured credential storage, **`encryptConnectionFields`** and **`decryptConnectionFields`** apply encryption to specific fields (`apiKey`, `accessToken`) in place, as defined in [lines 54-68](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/db/encryption.ts#L54-L68) and [lines 59-71](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/db/encryption.ts#L59-L71):

```typescript
import { encryptConnectionFields, decryptConnectionFields } from '@/src/lib/db/encryption';

const conn = {
  id: 'conn-123',
  provider: 'openai',
  apiKey: 'sk-secret-key',
  accessToken: null,
};

encryptConnectionFields(conn);      // conn.apiKey now encrypted
const plain = decryptConnectionFields(conn); // restores original

```

## Automatic Migration for Legacy Data

OmniRoute handles encrypted data evolution without manual intervention. When `decrypt` successfully processes a **legacy-encrypted value** (identified by missing `enc:v1` prefix or dynamic salt derivation), the module flags it for upgrade.

The next `encrypt` call on that value automatically writes it with the current static-salt format, as coordinated between [lines 78-84](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/db/encryption.ts#L78-L84) and [lines 122-133](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/db/encryption.ts#L122-L133). This **seamless migration** ensures:

- Zero downtime during algorithm updates
- Gradual fleet-wide re-encryption without batch jobs
- Consistent security posture across all stored credentials

## Development Mode and Optional Encryption

The encryption layer degrades gracefully when `STORAGE_ENCRYPTION_KEY` is unavailable. In [lines 47-53](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/db/encryption.ts#L47-L53) and [lines 87-99](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/db/encryption.ts#L87-L99), the module:

- Falls back to **plaintext passthrough** with a warning log
- Allows development and testing without key management infrastructure
- Makes encryption adoption **explicit and operator-controlled**

This design avoids silent security failures while supporting diverse deployment contexts.

## Summary

- **AES-256-GCM** provides authenticated encryption with 16-byte IVs and enforced authentication tags
- **scryptSync** with static salt derives deterministic 32-byte keys from `STORAGE_ENCRYPTION_KEY`
- **Prefixed ciphertext format** (`enc:v1:...`) enables versioning and component extraction
- **Graceful degradation** to plaintext when encryption key is absent, with explicit logging
- **Automatic migration** upgrades legacy dynamic-salt ciphertext on next write
- **Connection helpers** apply consistent protection to `apiKey` and `accessToken` fields

## Frequently Asked Questions

### What happens if the STORAGE_ENCRYPTION_KEY is compromised?

If `STORAGE_ENCRYPTION_KEY` is exposed, an attacker could derive the encryption key and decrypt any API keys they have database access to. However, the static salt means rotating the environment variable requires re-encrypting all stored credentials. OmniRoute does not currently implement automatic re-encryption on key rotation—operators must trigger this through application logic or database migration scripts.

### Why does OmniRoute use a static salt instead of random per-row salts?

The static salt (`"omniroute-field-encryption-v1"`) eliminates the need to store salt alongside ciphertext, reducing storage overhead and simplifying backup restoration. This trade-off accepts that identical passwords across deployments would produce identical keys, but since `STORAGE_ENCRYPTION_KEY` should be unique per deployment, this risk is mitigated. The design prioritizes operational simplicity over theoretical salt randomization benefits.

### How does OmniRoute prevent authentication tag forgery?

The implementation enforces a full 16-byte authentication tag through the `authTagLength: 16` option in `createDecipheriv`. Node.js will reject any ciphertext with a shorter or mismatched tag before returning decrypted data, preventing the **tag truncation attacks** that affected some GCM implementations. This is verified in the test suite at [[`tests/unit/webdav-server-3485.test.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/tests/unit/webdav-server-3485.test.ts)](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/tests/unit/webdav-server-3485.test.ts).

### Can I disable encryption for debugging purposes?

OmiRoute does not provide a configuration flag to disable encryption. However, if `STORAGE_ENCRYPTION_KEY` is unset, the module automatically falls back to plaintext storage with a warning log. For explicit debugging, you can temporarily remove the environment variable, though this is not recommended for production or staging environments handling real credentials.