API Key Encryption at Rest in OmniRoute: Best Practices and Implementation Guide
OmniRoute secures provider credentials using AES-256-GCM field-level encryption with scrypt-derived keys, storing secrets in a versioned format that supports automatic migration from legacy schemes.
The OmniRoute open-source routing platform stores sensitive provider credentials—including API keys, access tokens, and refresh tokens—in a local SQLite database. To prevent unauthorized access to these secrets, the codebase implements a comprehensive encryption strategy centered in src/lib/db/encryption.ts. This guide explains the architectural decisions, configuration requirements, and operational procedures that ensure your API keys remain confidential and tamper-evident while at rest.
Core Encryption Architecture
OmniRoute employs authenticated encryption to protect credential fields before they reach the database layer. The implementation prioritizes both security and operational continuity.
AES-256-GCM Implementation
The encryption module in src/lib/db/encryption.ts defines ALGORITHM = "aes-256-gcm" and AUTH_TAG_LENGTH = 16 to enforce Galois/Counter Mode with a full-length authentication tag [lines 30-38]. This mode provides confidentiality (preventing unauthorized reading) and integrity (detecting tampering) simultaneously.
The encrypt() function generates a random initialization vector (IV) for each operation, ensuring that identical API keys produce different ciphertexts. The resulting encrypted string follows the format enc:v1:<iv_hex>:<ciphertext_hex>:<authTag_hex>, making it easy to identify encrypted values programmatically [lines 5-6].
Key Derivation with Static Salt
Rather than using the raw environment variable as the encryption key, OmniRoute derives the key using scryptSync():
// From src/lib/db/encryption.ts
const STATIC_SALT = "omniroute-field-encryption-v1";
const derivedKey = scryptSync(secret, STATIC_SALT, KEY_LENGTH);
The getStaticKey() function implements this derivation [lines 41-66]. Using a static, versioned salt ensures that every encryption and decryption operation uses the identical derived key, eliminating key-mismatch bugs that plagued earlier dynamic-salt implementations.
Configuration and Environment Setup
Proper configuration of the encryption key is the most critical security step when deploying OmniRoute.
Setting STORAGE_ENCRYPTION_KEY
The system expects a STORAGE_ENCRYPTION_KEY environment variable containing a high-entropy secret. Generate a secure 32-byte key using standard tools:
export STORAGE_ENCRYPTION_KEY=$(openssl rand -base64 32)
OmniRoute reads process.env.STORAGE_ENCRYPTION_KEY at runtime. This key protects all stored credentials, making it the singular secret that safeguards your entire credential database.
Graceful Degradation Behavior
If STORAGE_ENCRYPTION_KEY is missing, the encryption module falls back to passthrough mode, storing credentials as plaintext while logging a warning [lines 62-64]. Additionally, when encryption fails due to runtime errors, the code returns the original plaintext rather than crashing, maintaining service availability while clearly indicating misconfiguration [lines 33-38].
Data Format and Versioning
Structured storage formats enable safe upgrades and prevent common operational errors.
The enc:v1 Prefix Convention
Every encrypted value carries the prefix enc:v1:, creating a clear versioning scheme. This convention allows the system to:
- Detect whether a value requires encryption before writing
- Identify the encryption version for future algorithm upgrades
- Distinguish encrypted blobs from legacy plaintext
Preventing Double Encryption
The encrypt() function short-circuits if the incoming plaintext already starts with the enc:v1: prefix [lines 20-22]. This prevents the creation of malformed nested ciphertexts and eliminates unnecessary CPU overhead during repeated save operations.
Migration and Backward Compatibility
OmniRoute maintains compatibility with data encrypted under previous schemes while providing utilities for seamless upgrades.
Legacy Key Support
The module retains a getLegacyDynamicKey() function for reading data encrypted with older dynamic-salt methods. During decryption, the system attempts the static key first, then falls back to the legacy key if that fails [lines 90-98]. This ensures existing installations can read older ciphertexts without immediate migration.
Automatic Migration Utilities
The migrateLegacyEncryptedString() function detects values encrypted with obsolete keys, decrypts them using the legacy method, and re-encrypts them with the current static key [lines 49-57]. This utility enables zero-downtime migration scripts that gradually upgrade the database to the current security standard.
Practical Implementation Examples
These patterns demonstrate how to interact with the encryption layer in production code.
Encrypting a Single API Key
import { encrypt } from "@/lib/db/encryption";
const rawKey = "sk-abcdef123456";
const encryptedKey = encrypt(rawKey);
// Returns: "enc:v1:a1b2c3...:x9y8z7...:tag..."
Encrypting Connection Objects
The encryptConnectionFields() helper applies encryption to all credential fields within a connection object:
import { encryptConnectionFields } from "@/lib/db/encryption";
const conn = {
provider: "openai",
apiKey: "sk-abcdef123456",
accessToken: null,
};
encryptConnectionFields(conn);
// conn.apiKey now contains the encrypted string
Decrypting After Database Retrieval
import { decryptConnectionFields } from "@/lib/db/encryption";
const row = await db.getConnection(providerId);
const plaintextConn = decryptConnectionFields(row);
// plaintextConn.apiKey contains the original API key
Running Legacy Migration Scripts
import { migrateLegacyEncryptedString } from "@/lib/db/encryption";
function migrateRow(row) {
const fields = ["apiKey", "accessToken", "refreshToken", "idToken"];
let updated = false;
for (const f of fields) {
const val = row[f];
if (val && val.startsWith("enc:v1:")) {
const { updated: changed, value } = migrateLegacyEncryptedString(val);
if (changed) {
row[f] = value;
updated = true;
}
}
}
return updated;
}
Summary
- Use AES-256-GCM as implemented in
src/lib/db/encryption.tsto ensure both confidentiality and integrity of stored credentials. - Configure
STORAGE_ENCRYPTION_KEYwith a high-entropy, base64-encoded 32-byte value before production deployment. - Leverage static salt derivation (
scryptSyncwithSTATIC_SALT) to ensure consistent key generation across all operations. - Store values in the
enc:v1:format to enable version detection and future algorithm upgrades. - Utilize helper functions like
encryptConnectionFields()anddecryptConnectionFields()to centralize encryption logic and prevent missed fields. - Migrate legacy data using
migrateLegacyEncryptedString()to bring older installations up to current security standards.
Frequently Asked Questions
What happens if I forget to set STORAGE_ENCRYPTION_KEY?
OmniRoute enters passthrough mode and stores credentials as plaintext while logging a warning. The application continues to function, but your API keys remain unencrypted in the SQLite database, exposing them to anyone with file system access.
Why does OmniRoute use a static salt instead of a random one?
The static salt "omniroute-field-encryption-v1" ensures deterministic key derivation. This prevents key-mismatch bugs where the same password produces different keys across restarts, which was a critical issue in earlier dynamic-salt implementations. The versioned salt also acts as a schema marker for future upgrades.
Can I rotate the encryption key without losing access to existing credentials?
Key rotation requires re-encrypting existing data. You must decrypt all values using the old key and re-encrypt them with the new key. The current codebase does not provide automatic key rotation; you would need to implement a migration script that reads all rows, decrypts them with the current key, changes the environment variable, and re-encrypts them with the new key.
How does the system prevent tampering with encrypted API keys?
AES-256-GCM includes authentication tags that are verified during decryption. If an attacker modifies the ciphertext, the authentication tag validation fails, and the decryption function rejects the tampered data. This prevents tag-truncation attacks and other forgeries that could compromise the credential database.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →