From 7183404bd8ce6ffe01a8cd2f8815f13b3f798b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Emre=20Kabakc=C4=B1?= Date: Fri, 15 May 2026 01:32:29 +0100 Subject: [PATCH] fix(core): accept URL-safe base64 in ENCRYPTION_KEY validator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #692 ("sec: hardening sweep") tightened `getEncryptionKey()` to require strict canonical base64 (`[A-Za-z0-9+/]`). Historical keys generated as `openssl rand -base64 32 | tr +/ -_` use the URL-safe alphabet (`-_`) and the validator started rejecting them — even though they decode to the same 32 bytes. Effect: every code path that calls `encrypt()` / `decrypt()` 500s on prod, including agent session creation. `lobu chat` against any agent in prod returns "Internal server error" with this in app-pod logs: [worker-auth] Error verifying token: ENCRYPTION_KEY must be a canonical base64 or hex encoded 32-byte key. at getEncryptionKey at encrypt at generateWorkerToken at /lobu/api/v1/agents Fix: add a parallel branch that accepts URL-safe base64 (alphabet `[A-Za-z0-9_-]`, no padding), with the same round-trip + length check so typo'd keys are still rejected. No key rotation needed. Test added: 32-byte URL-safe base64 key round-trips encrypt/decrypt. --- .../__tests__/encryption-key-validation.test.ts | 9 +++++++++ packages/core/src/utils/encryption.ts | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/core/src/__tests__/encryption-key-validation.test.ts b/packages/core/src/__tests__/encryption-key-validation.test.ts index 228a4c13e..02e4c773e 100644 --- a/packages/core/src/__tests__/encryption-key-validation.test.ts +++ b/packages/core/src/__tests__/encryption-key-validation.test.ts @@ -42,6 +42,15 @@ describe("ENCRYPTION_KEY validation", () => { expect(decrypt(enc)).toBe("base64 secret"); }); + test("valid 32-byte URL-safe base64 key round-trips encrypt/decrypt", () => { + // Historical keys were sometimes generated with `openssl rand -base64 32 | + // tr +/ -_` and stored in URL-safe form (alphabet [A-Za-z0-9_-], no + // padding). Same 32 bytes — must be accepted. + process.env.ENCRYPTION_KEY = Buffer.alloc(32, 99).toString("base64url"); + const enc = encrypt("urlsafe secret"); + expect(decrypt(enc)).toBe("urlsafe secret"); + }); + test("valid 64-char hex key round-trips encrypt/decrypt", () => { process.env.ENCRYPTION_KEY = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; diff --git a/packages/core/src/utils/encryption.ts b/packages/core/src/utils/encryption.ts index ecb2a04cd..d1ab5ad56 100644 --- a/packages/core/src/utils/encryption.ts +++ b/packages/core/src/utils/encryption.ts @@ -34,6 +34,21 @@ function getEncryptionKey(): Buffer { } } + // Try as URL-safe base64 (alphabet [A-Za-z0-9_-], no padding). Historically + // some keys were generated as `openssl rand -base64 32 | tr +/ -_` and stored + // in this form; same 32 bytes, just a different alphabet. Apply the same + // round-trip check so typos still get rejected. + if (/^[A-Za-z0-9_-]+$/.test(key)) { + const urlsafeBuffer = Buffer.from(key, "base64url"); + if ( + urlsafeBuffer.length === 32 && + urlsafeBuffer.toString("base64url") === key + ) { + cachedKey = urlsafeBuffer; + return urlsafeBuffer; + } + } + // Try as hex (must be exactly 64 hex characters for 32 bytes), again // verifying the round-trip so partially-valid input is rejected. if (/^[0-9a-fA-F]+$/.test(key) && key.length % 2 === 0) {