diff --git a/apps/web/lib/buildNonce.test.ts b/apps/web/lib/buildNonce.test.ts new file mode 100644 index 0000000000..46c7f6c26e --- /dev/null +++ b/apps/web/lib/buildNonce.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect } from "vitest"; + +import { buildNonce } from "./buildNonce"; + +describe("buildNonce", () => { + it("should return an empty string for an empty array", () => { + const nonce = buildNonce(new Uint8Array()); + + expect(nonce).toEqual(""); + expect(atob(nonce).length).toEqual(0); + }); + + it("should return a base64 string for values from 0 to 63", () => { + const array = Array(22) + .fill(0) + .map((_, i) => i); + const nonce = buildNonce(new Uint8Array(array)); + + expect(nonce.length).toEqual(24); + expect(nonce).toEqual("ABCDEFGHIJKLMNOPQRSTQQ=="); + + expect(atob(nonce).length).toEqual(16); + }); + + it("should return a base64 string for values from 64 to 127", () => { + const array = Array(22) + .fill(0) + .map((_, i) => i + 64); + const nonce = buildNonce(new Uint8Array(array)); + + expect(nonce.length).toEqual(24); + expect(nonce).toEqual("ABCDEFGHIJKLMNOPQRSTQQ=="); + + expect(atob(nonce).length).toEqual(16); + }); + + it("should return a base64 string for values from 128 to 191", () => { + const array = Array(22) + .fill(0) + .map((_, i) => i + 128); + const nonce = buildNonce(new Uint8Array(array)); + + expect(nonce.length).toEqual(24); + expect(nonce).toEqual("ABCDEFGHIJKLMNOPQRSTQQ=="); + + expect(atob(nonce).length).toEqual(16); + }); + + it("should return a base64 string for values from 192 to 255", () => { + const array = Array(22) + .fill(0) + .map((_, i) => i + 192); + const nonce = buildNonce(new Uint8Array(array)); + + expect(nonce.length).toEqual(24); + expect(nonce).toEqual("ABCDEFGHIJKLMNOPQRSTQQ=="); + + expect(atob(nonce).length).toEqual(16); + }); + + it("should return a base64 string for values from 0 to 42", () => { + const array = Array(22) + .fill(0) + .map((_, i) => 2 * i); + const nonce = buildNonce(new Uint8Array(array)); + + expect(nonce.length).toEqual(24); + expect(nonce).toEqual("ACEGIKMOQSUWYacegikmgg=="); + + expect(atob(nonce).length).toEqual(16); + }); + + it("should return a base64 string for 0 values", () => { + const array = Array(22) + .fill(0) + .map(() => 0); + const nonce = buildNonce(new Uint8Array(array)); + + expect(nonce.length).toEqual(24); + expect(nonce).toEqual("AAAAAAAAAAAAAAAAAAAAAA=="); + + expect(atob(nonce).length).toEqual(16); + }); + + it("should return a base64 string for 0xFF values", () => { + const array = Array(22) + .fill(0) + .map(() => 0xff); + const nonce = buildNonce(new Uint8Array(array)); + + expect(nonce.length).toEqual(24); + expect(nonce).toEqual("////////////////////ww=="); + + expect(atob(nonce).length).toEqual(16); + }); +}); diff --git a/apps/web/lib/buildNonce.ts b/apps/web/lib/buildNonce.ts new file mode 100644 index 0000000000..211371dbc7 --- /dev/null +++ b/apps/web/lib/buildNonce.ts @@ -0,0 +1,46 @@ +const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +/* +The buildNonce array allows a randomly generated 22-unsigned-byte array +and returns a 24-ASCII character string that mimics a base64-string. +*/ + +export const buildNonce = (uint8array: Uint8Array): string => { + // the random uint8array should contain 22 bytes + // 22 bytes mimic the base64-encoded 16 bytes + // base64 encodes 6 bits (log2(64)) with 8 bits (64 allowed characters) + // thus ceil(16*8/6) gives us 22 bytes + if (uint8array.length != 22) { + return ""; + } + + // for each random byte, we take: + // a) only the last 6 bits (so we map them to the base64 alphabet) + // b) for the last byte, we are interested in two bits + // explaination: + // 16*8 bits = 128 bits of information (order: left->right) + // 22*6 bits = 132 bits (order: left->right) + // thus the last byte has 4 redundant (least-significant, right-most) bits + // it leaves the last byte with 2 bits of information before the redundant bits + // so the bitmask is 0x110000 (2 bits of information, 4 redundant bits) + const bytes = uint8array.map((value, i) => { + if (i < 20) { + return value & 0b111111; + } + + return value & 0b110000; + }); + + const nonceCharacters: string[] = []; + + bytes.forEach((value) => { + nonceCharacters.push(BASE64_ALPHABET.charAt(value)); + }); + + // base64-encoded strings can be padded with 1 or 2 `=` + // since 22 % 4 = 2, we pad with two `=` + nonceCharacters.push("=="); + + // the end result has 22 information and 2 padding ASCII characters = 24 ASCII characters + return nonceCharacters.join(""); +}; diff --git a/apps/web/lib/csp.ts b/apps/web/lib/csp.ts index 830ad7ffff..257f0d2dc7 100644 --- a/apps/web/lib/csp.ts +++ b/apps/web/lib/csp.ts @@ -1,10 +1,11 @@ -import crypto from "crypto"; import type { IncomingMessage, OutgoingMessage } from "http"; import { z } from "zod"; import { IS_PRODUCTION } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants"; +import { buildNonce } from "@lib/buildNonce"; + function getCspPolicy(nonce: string) { //TODO: Do we need to explicitly define it in turbo.json const CSP_POLICY = process.env.CSP_POLICY; @@ -59,7 +60,7 @@ export function csp(req: IncomingMessage | null, res: OutgoingMessage | null) { } const CSP_POLICY = process.env.CSP_POLICY; const cspEnabledForInstance = CSP_POLICY; - const nonce = crypto.randomBytes(16).toString("base64"); + const nonce = buildNonce(crypto.getRandomValues(new Uint8Array(22))); const parsedUrl = new URL(req.url, "http://base_url"); const cspEnabledForPage = cspEnabledForInstance && isPagePathRequest(parsedUrl);