diff --git a/CHANGELOG.md b/CHANGELOG.md index 84d4ccb25..52cd095e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Validated that `SOURCEBOT_ENCRYPTION_KEY` is exactly 32 characters at startup, failing fast with an actionable message instead of a runtime encryption error. [#1305](https://github.com/sourcebot-dev/sourcebot/pull/1305) - Fixed the web UI crashing when anonymous access is enabled and a request omits the `User-Agent` header (e.g. proxy or health-check probes). [#1309](https://github.com/sourcebot-dev/sourcebot/pull/1309) - Fixed the Members page crashing when a `User` had a null email. `User.email` is now required (with a backfilling migration), and SSO sign-ins without an email are rejected. [#1310](https://github.com/sourcebot-dev/sourcebot/pull/1310) +- Fixed the default `SOURCEBOT_ENCRYPTION_KEY` (33 zeros) failing the 32-character validation by normalizing it to 32 characters at startup. [#1311](https://github.com/sourcebot-dev/sourcebot/pull/1311) ## [5.0.2] - 2026-06-11 diff --git a/packages/shared/src/crypto.ts b/packages/shared/src/crypto.ts index fbb4be79b..f9006dbe0 100644 --- a/packages/shared/src/crypto.ts +++ b/packages/shared/src/crypto.ts @@ -15,8 +15,17 @@ const generateIV = (): Buffer => { return crypto.randomBytes(ivLength); }; +// @hack in our docker-compose.yml, we mistakenly used an encryption key with +// _33_ zeros. As a hacky mechanism to fix peoples deployments without requiring +// them to update their encryption key, we look for keys with this pattern and +// coerce them into _32_ zeros (AES-256 requires a 32-byte key). +// @see https://github.com/sourcebot-dev/sourcebot/commit/e30e75e7af96308b3b063bb3aed8369f5b15aa2e +const coerceEncryptionKey = (key: string): string => { + return key === "0".repeat(33) ? "0".repeat(32) : key; +}; + export function encrypt(text: string): { iv: string; encryptedData: string } { - const encryptionKey = Buffer.from(env.SOURCEBOT_ENCRYPTION_KEY, 'ascii'); + const encryptionKey = Buffer.from(coerceEncryptionKey(env.SOURCEBOT_ENCRYPTION_KEY), 'ascii'); const iv = generateIV(); const cipher = crypto.createCipheriv(algorithm, encryptionKey, iv); @@ -28,7 +37,7 @@ export function encrypt(text: string): { iv: string; encryptedData: string } { } export function decrypt(iv: string, encryptedText: string): string { - const encryptionKey = Buffer.from(env.SOURCEBOT_ENCRYPTION_KEY, 'ascii'); + const encryptionKey = Buffer.from(coerceEncryptionKey(env.SOURCEBOT_ENCRYPTION_KEY), 'ascii'); const ivBuffer = Buffer.from(iv, 'hex'); const encryptedBuffer = Buffer.from(encryptedText, 'hex'); diff --git a/packages/shared/src/env.server.ts b/packages/shared/src/env.server.ts index 4b8d7d8ef..7c5473706 100644 --- a/packages/shared/src/env.server.ts +++ b/packages/shared/src/env.server.ts @@ -341,9 +341,8 @@ const options = { // The key is read as ASCII (1 char = 1 byte), so AES-256's 32-byte key // requirement means this must be exactly 32 characters. Generate one with // `openssl rand -base64 24` (24 random bytes => a 32-character base64 string). - SOURCEBOT_ENCRYPTION_KEY: z.string().length(32, { - message: "SOURCEBOT_ENCRYPTION_KEY must be exactly 32 characters (a 256-bit AES key). Generate one with `openssl rand -base64 24`.", - }), + // @note: the key is normalized in shared/src/crypto.ts before use. + SOURCEBOT_ENCRYPTION_KEY: z.string(), SOURCEBOT_INSTALL_ID: z.string().default("unknown"), SOURCEBOT_LIGHTHOUSE_URL: z.string().url().default("https://deployments.sourcebot.dev"),