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

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/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 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.

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 and lines 84-92, 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.

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:

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 and lines 126-138:

  • Authentication failures return null rather than throwing
  • The quiet option suppresses error logging for expected failures
  • Legacy format detection triggers automatic re-encryption
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 and lines 59-71:

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 and lines 122-133. 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 and lines 87-99, 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/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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →