How Provider API Keys Are Encrypted at Rest Using AES-256-GCM in OmniRoute's SQLite Database

OmniRoute encrypts sensitive provider credentials using AES-256-GCM in src/lib/db/encryption.ts, deriving a 32-byte master key from the environment variable OMNIROUTE_DB_ENCRYPTION_KEY and storing the IV, ciphertext, and auth tag concatenated as a BLOB in SQLite.

OmniRoute is an open-source routing layer that stores provider API keys, OAuth tokens, and client secrets in a local SQLite database. To ensure these credentials remain secure even if the database file is compromised, the project implements AES-256-GCM encryption at rest according to the source code in diegosouzapw/OmniRoute. This article explains the complete encryption workflow, from the low-level crypto primitives to the high-level database operations that automatically secure your connection strings.

The AES-256-GCM Encryption Architecture

Core Encryption Module

The encryption logic lives in src/lib/db/encryption.ts. This module exports two low-level helpers—encryptValue and decryptValue—that wrap Node.js's native crypto API to provide authenticated encryption using the aes-256-gcm algorithm.

Every encryption operation generates a random 12-byte initialization vector (IV) via crypto.randomBytes(12). GCM mode produces a 16-byte authentication tag that provides integrity checking. The final stored value is a binary BLOB containing the concatenation: IV‖ciphertext‖authTag.

Key Management and Environment Configuration

The master encryption key is sourced from the environment variable OMNIROUTE_DB_ENCRYPTION_KEY. This key must be exactly 32 bytes (256 bits) to match the AES-256 key size requirement. The application validates this key at startup before initializing the database connection, ensuring the encryption layer is ready before any provider credentials are persisted.

High-Level Field Encryption

The module also exports encryptConnectionFields and decryptConnectionFields, which walk through provider connection objects and target fields marked as sensitive—such as apiKey, clientSecret, and oauthToken. These high-level helpers replace plaintext secrets with encrypted BLOBs before the record reaches the database.

Database Integration Layer

Encrypting Fields Before Storage

When inserting a new provider connection, the database layer in src/lib/db/core.ts invokes the encryption helper to transform sensitive fields:

// src/lib/db/core.ts
import { encryptConnectionFields } from './encryption.js';

export function insertProviderConnection(conn: ProviderConnection) {
  const encrypted = encryptConnectionFields(conn);
  const stmt = db.prepare(`
    INSERT INTO provider_connections (providerId, modelId, encryptedFields)
    VALUES (?, ?, ?)
  `);
  stmt.run(conn.providerId, conn.modelId, encrypted);
}

The raw API keys never touch the SQLite file; they are replaced by the encrypted BLOB containing the IV, ciphertext, and authentication tag.

Transparent Decryption on Retrieval

When reading records back, the reverse process occurs automatically. The decryptConnectionFields function decrypts the BLOBs to plaintext strings before returning the object to the application code:

// src/lib/db/core.ts
import { decryptConnectionFields } from './encryption.js';

export function getProviderConnection(id: number) {
  const row = db.prepare('SELECT * FROM provider_connections WHERE id = ?').get(id);
  return decryptConnectionFields(row);
}

Practical Implementation Examples

Inserting a Provider with an API Key

import { insertProviderConnection } from '@/lib/db/core';
import { ProviderConnection } from '@/lib/db/types';

const conn: ProviderConnection = {
  providerId: 'openai',
  modelId: 'gpt-4o',
  apiKey: 'sk-your-secret-key', // Automatically encrypted
  clientSecret: 'oauth-secret'    // Automatically encrypted
};

await insertProviderConnection(conn);
// The API key is stored as an encrypted BLOB; plaintext never touches disk

Retrieving Decrypted Credentials

import { getProviderConnection } from '@/lib/db/core';

const stored = await getProviderConnection(42);
console.log(stored.apiKey); // Plaintext: 'sk-your-secret-key'

Manual Encryption Operations

import { encryptValue, decryptValue } from '@/lib/db/encryption';

const secret = 'sensitive-data';
const encrypted: Buffer = encryptValue(secret); // Returns Buffer(IV‖cipher‖tag)
const decrypted: string = decryptValue(encrypted); // Returns 'sensitive-data'

Security Benefits of GCM Mode

AES-256-GCM provides authenticated encryption, meaning any tampering with the stored ciphertext will cause decryption to fail. This integrity protection is validated in tests/unit/db-encryption.test.ts, which ensures that corrupted data or modified auth tags trigger exceptions that prevent the application from using compromised credentials.

Summary

  • OmniRoute uses AES-256-GCM authenticated encryption for all provider credentials stored in SQLite
  • The encryption module at src/lib/db/encryption.ts handles low-level crypto operations via encryptValue and decryptValue
  • A 32-byte master key from OMNIROUTE_DB_ENCRYPTION_KEY drives the encryption process
  • Data is stored as BLOBs containing IV‖ciphertext‖authTag to ensure both confidentiality and integrity
  • The database layer in src/lib/db/core.ts automatically encrypts on write and decrypts on read via encryptConnectionFields and decryptConnectionFields

Frequently Asked Questions

What happens if the OMNIROUTE_DB_ENCRYPTION_KEY is lost?

If the master key is lost or changed, existing encrypted data becomes permanently inaccessible. OmniRoute does not implement key recovery or escrow mechanisms; you must re-enter all provider credentials through the configuration interface to regenerate encrypted entries with the new key.

Why does OmniRoute use AES-256-GCM specifically?

AES-256-GCM provides both confidentiality and authentication in a single cryptographic operation. The 16-byte authentication tag protects against tampering attacks where an attacker might modify the encrypted database file to inject malicious credentials, ensuring the application detects any modification before using the decrypted values.

Are all fields in the database encrypted?

No. Only sensitive fields explicitly marked as secrets—such as apiKey, clientSecret, and oauthToken—are encrypted according to the schema definitions in src/lib/db/providers.ts. Metadata fields like providerId and modelId remain in plaintext to support indexing, querying, and foreign key relationships without decryption overhead.

How is the encryption key protected in memory?

The key is loaded once at startup from the environment variable and held in memory by the Node.js process. OmniRoute relies on the host operating system's memory protection mechanisms and does not write the key to logs, stack traces, or crash dumps. Process memory isolation prevents other applications from accessing the key while the OmniRoute service is running.

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 →