OmniRoute Provider Credentials Encryption at Rest: AES-256-GCM Security Implementation

OmniRoute encrypts all provider credentials using AES-256-GCM with a master key derived from STORAGE_ENCRYPTION_KEY, storing ciphertext in SQLite with automatic legacy migration and field-level protection.

This guide examines how diegosouzapw/OmniRoute implements encryption of sensitive provider data at rest. The security architecture centers on a dedicated encryption module with deterministic key derivation, versioned payloads, and transparent migration from older schemes.

Encryption Architecture and Key Management

All credential encryption flows through src/lib/db/encryption.ts, a focused module designed for auditability and minimal attack surface. The system relies on environment variable configuration with graceful degradation.

Required Environment Variables

Variable Purpose Behavior if Missing
STORAGE_ENCRYPTION_KEY Primary AES-256-GCM master key source Encryption disabled; plaintext storage
STORAGE_ENCRYPTION_KEY_VERSION Algorithm version label (e.g., v1) Defaults to v1
OMNIROUTE_CRYPT_KEY / OMNIROUTE_API_KEY_BASE64 Legacy aliases for backward compatibility Checked if primary variable unset

Loss of the configured key renders encrypted columns permanently unrecoverable—the design explicitly favors security over recoverability.

Key Derivation with scrypt

The master key is derived from the raw environment variable using scrypt with a static salt of "omniroute-field-encryption-v1":

// src/lib/db/encryption.ts#L41
// Deterministic derivation ensures consistent keys for deduplication
const key = scryptSync(
  Buffer.from(masterKey, 'utf-8'),
  Buffer.from('omniroute-field-encryption-v1'),
  32
);

The static salt choice is intentional: identical secrets produce identical encryption keys, simplifying data migration and field deduplication across the database.

Encryption Flow: From Plaintext to Ciphertext

The encrypt() function implements authenticated encryption with associated data (AEAD) via AES-256-GCM.

Serialization Format

Encrypted values follow a structured, versioned string format:


enc:v1:<iv-hex>:<cipher-hex>:<authTag-hex>

  • enc:v1 — Algorithm identifier for future-proof upgrades
  • IV (12 bytes random) — Ensures unique ciphertexts for identical plaintexts
  • Ciphertext — AES-256-GCM encrypted payload
  • Auth tag (16 bytes) — Integrity verification preventing tampering

Implementation at lines 135-141 of src/lib/db/encryption.ts handles the construction:

// Actual usage pattern from the codebase
import { encrypt, decrypt } from '@/lib/db/encryption';

const secret = 'sk-my-super-private-api-key';
const encrypted = encrypt(secret);     // → "enc:v1:a3f2...:9e8b...:c4d1..."
const recovered = decrypt(encrypted);  // → "sk-my-super-private-api-key"

Decryption and Fail-Safe Handling

The decrypt() function (lines 181-192) enforces defensive failure modes:

  • Missing key — Returns null, logs: "Found encrypted data but STORAGE_ENCRYPTION_KEY is not set"
  • Malformed payload — Returns null after parsing failure
  • Auth tag mismatch — Cryptographic failure returns null

This design never exposes ciphertext or throws exceptions that could leak implementation details. Callers must explicitly handle null returns, preventing accidental plaintext processing of encrypted material.

// src/lib/db/encryption.ts#L181-L192
// Decrypt returns null on any failure path
const decrypted = decrypt(storedValue);
if (decrypted === null) {
  // Handle missing key or corrupted data appropriately
}

Automatic Legacy Migration

OmniRoute's encryption module transparently upgrades data from earlier releases that used XOR-based obfuscation:

  1. Detectiondecrypt() recognizes legacy values by absence of enc:v1 prefix
  2. Decryption — XOR scheme decoded using legacy key
  3. Re-encryption — Immediate AES-256-GCM encryption with current static-salt key
  4. Persistence — Updated ciphertext stored via encryptConnectionFields()

This migration triggers at lines 224-231 and runs automatically during normal database operations without downtime.

Database-Wide Migration at Startup

For comprehensive coverage, src/lib/db/core.ts (lines 875-896) implements a startup migration loop:

// src/lib/db/core.ts#L875-L896
// Scans provider_connections table for legacy fields
// Creates backup before any mutation
// Re-encrypts to canonical format

This guarantees single canonical encryption format across the entire schema, eliminating security downgrade risks from partially migrated deployments.

Field-Level Integration in Database Layer

High-level helpers ensure all credential columns are protected consistently. The encryptConnectionFields() function (referenced at src/lib/db/providers.ts#L108-L112 and L437-L445) processes:

  • apiKey
  • accessToken
  • refreshToken
  • idToken

Practical Usage Examples

Storing a new provider connection:

import { encryptConnectionFields } from '@/lib/db/encryption';
import { insertProviderConnection } from '@/lib/db/providers';

async function addConnection(raw: {
  providerId: string;
  apiKey: string;
  refreshToken?: string;
}) {
  // All credential fields automatically encrypted
  const encrypted = encryptConnectionFields({
    apiKey: raw.apiKey,
    refreshRefreshToken: raw.refreshToken,
  });
  
  await insertProviderConnection({
    providerId: raw.providerId,
    ...encrypted,
  });
}

Retrieving and decrypting credentials:

import { decrypt } from '@/lib/db/encryption';
import { getProviderConnection } from '@/lib/db/providers';

async function getApiKey(connectionId: number) {
  const conn = await getProviderConnection(connectionId);
  // decrypt returns plaintext or null; never throws
  return conn.apiKey ? decrypt(conn.apiKey) : null;
}

Security Guarantees and Operational Considerations

Cryptographic Strengths

  • AES-256-GCM — NIST-approved, provides both confidentiality and integrity
  • Random per-value IVs — Prevents pattern analysis and replay attacks
  • Authenticated encryption — Auth tag detects any ciphertext tampering

Operational Security

  • Key isolation — Master key exists only in process environment, never in source control
  • Versioned payloadsenc:v1 prefix enables algorithm agility for post-quantum or rotated schemes
  • Audit logging — Operations logged without secret exposure; failure modes are explicit
  • Backup discipline — Startup migration creates pre-mutation backups automatically

Critical Warnings

  • Key loss = data loss: No key escrow or recovery mechanism exists by design
  • Static salt trade-off: Enables deduplication but means identical keys across all fields for a given master secret
  • Legacy XOR exposure: Pre-migration backups may contain weakly protected data; secure deletion recommended

Key Implementation Files

File Security Role
src/lib/db/encryption.ts Core AES-256-GCM engine (encrypt, decrypt, encryptConnectionFields, legacy migration)
src/lib/db/providers.ts Integration layer enforcing field-level encryption on all credential writes
src/lib/db/core.ts Database bootstrap with bulk legacy migration at startup
docs/reference/ENVIRONMENT.md Variable documentation and key generation guidance
docs/ops/DATABASE_GUIDE.md Operational procedures for encrypted backups and rotation

Summary

  • AES-256-GCM with scrypt key derivation protects all provider credentials at rest in OmniRoute's SQLite database
  • STORAGE_ENCRYPTION_KEY drives encryption; loss renders data permanently inaccessible
  • Versioned payload format (enc:v1) enables future algorithm upgrades without breaking changes
  • Automatic legacy migration transparently upgrades XOR-obfuscated data to modern cryptography
  • Fail-safe decryption returns null on any failure, preventing accidental exposure
  • Field-level integration ensures consistent protection across apiKey, accessToken, refreshToken, and idToken columns

Frequently Asked Questions

What encryption algorithm does OmniRoute use for provider credentials?

OmniRoute uses AES-256-GCM authenticated encryption for all field-level protection of provider credentials at rest. The implementation in src/lib/db/encryption.ts generates random 12-byte IVs, encrypts UTF-8 plaintext, and appends 16-byte authentication tags for integrity verification. The versioned enc:v1 prefix allows future algorithm agility.

How is the encryption key derived and stored?

The encryption key is derived at runtime using scrypt from the STORAGE_ENCRYPTION_KEY environment variable with a static salt ("omniroute-field-encryption-v1"). The key never persists to disk—it exists only in process memory during operations. This design requires operators to manage the environment variable separately from the codebase, typically via secrets management systems.

What happens if the encryption key is lost or corrupted?

Encrypted data becomes permanently unrecoverable. OmniRoute implements no key escrow, backup key mechanisms, or recovery procedures by explicit security design. The decrypt() function returns null when keys are missing, and documentation in docs/ops/DATABASE_GUIDE.md warns operators to implement independent backup strategies for the master key itself.

How does OmniRoute handle credentials encrypted with older versions?

Automatic transparent migration occurs through two mechanisms: (1) the decrypt() function detects legacy XOR-obfuscated values and immediately re-encrypts them with AES-256-GCM after successful decryption, and (2) startup routines in src/lib/db/core.ts scan the entire provider_connections table to migrate any remaining legacy fields before normal operation begins.

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 →