From 951eb15dfe7e409c50ea679e30e6cff4c7661700 Mon Sep 17 00:00:00 2001 From: Bereket Engida Date: Fri, 6 Dec 2024 20:44:28 +0300 Subject: [PATCH] chore: add base 64 utility --- .../better-auth/src/crypto/base64.test.ts | 77 +++++ packages/better-auth/src/crypto/base64.ts | 322 ++++++++++++++++++ 2 files changed, 399 insertions(+) create mode 100644 packages/better-auth/src/crypto/base64.test.ts create mode 100644 packages/better-auth/src/crypto/base64.ts diff --git a/packages/better-auth/src/crypto/base64.test.ts b/packages/better-auth/src/crypto/base64.test.ts new file mode 100644 index 0000000000..326c441681 --- /dev/null +++ b/packages/better-auth/src/crypto/base64.test.ts @@ -0,0 +1,77 @@ +import { expect, test } from "vitest"; +import { base64 } from "./base64"; + +test("encodeBase64()", () => { + expect(base64.encode(new Uint8Array())).toBe(""); + for (let i = 1; i <= 100; i++) { + const bytes = new Uint8Array(i); + crypto.getRandomValues(bytes); + expect(base64.encode(bytes)).toBe(Buffer.from(bytes).toString("base64")); + } +}); + +test("encodeBase64NoPadding()", () => { + expect(base64.encode(new Uint8Array())).toBe(""); + for (let i = 1; i <= 100; i++) { + const bytes = new Uint8Array(i); + crypto.getRandomValues(bytes); + expect( + base64.encode(bytes, { + ignorePadding: true, + }), + ).toBe(base64.encode(bytes).replaceAll("=", "")); + } +}); + +test("decodeBase64()", () => { + expect(base64.decode("")).toStrictEqual(new Uint8Array()); + for (let i = 1; i <= 100; i++) { + const bytes = new Uint8Array(i); + crypto.getRandomValues(bytes); + expect(base64.decode(base64.encode(bytes))).toStrictEqual(bytes); + } +}); + +test("decodeBase64IgnorePadding()", () => { + expect( + base64.decode("", { + ignorePadding: true, + }), + ).toStrictEqual(new Uint8Array()); + for (let i = 1; i <= 100; i++) { + const bytes = new Uint8Array(i); + crypto.getRandomValues(bytes); + expect( + base64.decode( + base64.encode(bytes, { + ignorePadding: true, + }), + { + ignorePadding: true, + }, + ), + ).toStrictEqual(bytes); + } + // includes padding but invalid padding count + for (let i = 1; i <= 100; i++) { + const bytes = new Uint8Array(i); + crypto.getRandomValues(bytes); + expect( + base64.decode(base64.encode(bytes).replace("=", ""), { + ignorePadding: true, + }), + ).toStrictEqual(bytes); + } +}); + +test("decodeBase64() throws on invalid padding", () => { + expect(() => base64.decode("qqo")).toThrowError(); + expect(() => base64.decode("qqp=")).toThrowError(); + expect(() => base64.decode("q===")).toThrowError(); + expect(() => base64.decode("====")).toThrowError(); + expect(() => base64.decode("=")).toThrowError(); + expect(() => base64.decode("q=q=")).toThrowError(); + expect(() => base64.decode("qqqqq===")).toThrowError(); + expect(() => base64.decode("qqqq====")).toThrowError(); + expect(() => base64.decode("qqqqq=qq")).toThrowError(); +}); diff --git a/packages/better-auth/src/crypto/base64.ts b/packages/better-auth/src/crypto/base64.ts new file mode 100644 index 0000000000..0adc0d11ab --- /dev/null +++ b/packages/better-auth/src/crypto/base64.ts @@ -0,0 +1,322 @@ +//https://github.com/oslo-project/encoding/blob/main/src/base64.ts + +export const base64 = { + decode: ( + data: string, + config?: { + ignorePadding?: boolean; + url?: boolean; + }, + ) => { + if (config?.url) { + if (config?.ignorePadding) { + return decodeBase64urlIgnorePadding(data); + } + return decodeBase64url(data); + } + if (config?.ignorePadding) { + return decodeBase64IgnorePadding(data); + } + return decodeBase64(data); + }, + encode: ( + data: Uint8Array | string, + config?: { + ignorePadding?: boolean; + url?: boolean; + }, + ) => { + const bytes = + typeof data === "string" ? new TextEncoder().encode(data) : data; + if (config?.url) { + if (config?.ignorePadding) { + return encodeBase64urlNoPadding(bytes); + } + return encodeBase64url(bytes); + } + if (config?.ignorePadding) { + return encodeBase64NoPadding(bytes); + } + return encodeBase64(bytes); + }, +}; + +function encodeBase64(bytes: Uint8Array): string { + return encodeBase64_internal(bytes, base64Alphabet, EncodingPadding.Include); +} + +function encodeBase64NoPadding(bytes: Uint8Array): string { + return encodeBase64_internal(bytes, base64Alphabet, EncodingPadding.None); +} + +function encodeBase64url(bytes: Uint8Array): string { + return encodeBase64_internal( + bytes, + base64urlAlphabet, + EncodingPadding.Include, + ); +} + +function encodeBase64urlNoPadding(bytes: Uint8Array): string { + return encodeBase64_internal(bytes, base64urlAlphabet, EncodingPadding.None); +} + +function encodeBase64_internal( + bytes: Uint8Array, + alphabet: string, + padding: EncodingPadding, +): string { + let result = ""; + for (let i = 0; i < bytes.byteLength; i += 3) { + let buffer = 0; + let bufferBitSize = 0; + for (let j = 0; j < 3 && i + j < bytes.byteLength; j++) { + buffer = (buffer << 8) | bytes[i + j]; + bufferBitSize += 8; + } + for (let j = 0; j < 4; j++) { + if (bufferBitSize >= 6) { + result += alphabet[(buffer >> (bufferBitSize - 6)) & 0x3f]; + bufferBitSize -= 6; + } else if (bufferBitSize > 0) { + result += alphabet[(buffer << (6 - bufferBitSize)) & 0x3f]; + bufferBitSize = 0; + } else if (padding === EncodingPadding.Include) { + result += "="; + } + } + } + return result; +} + +const base64Alphabet = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +const base64urlAlphabet = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +function decodeBase64(encoded: string): Uint8Array { + return decodeBase64_internal( + encoded, + base64DecodeMap, + DecodingPadding.Required, + ); +} + +function decodeBase64IgnorePadding(encoded: string): Uint8Array { + return decodeBase64_internal( + encoded, + base64DecodeMap, + DecodingPadding.Ignore, + ); +} + +function decodeBase64url(encoded: string): Uint8Array { + return decodeBase64_internal( + encoded, + base64urlDecodeMap, + DecodingPadding.Required, + ); +} + +function decodeBase64urlIgnorePadding(encoded: string): Uint8Array { + return decodeBase64_internal( + encoded, + base64urlDecodeMap, + DecodingPadding.Ignore, + ); +} + +function decodeBase64_internal( + encoded: string, + decodeMap: Record, + padding: DecodingPadding, +): Uint8Array { + const result = new Uint8Array(Math.ceil(encoded.length / 4) * 3); + let totalBytes = 0; + for (let i = 0; i < encoded.length; i += 4) { + let chunk = 0; + let bitsRead = 0; + for (let j = 0; j < 4; j++) { + if (padding === DecodingPadding.Required && encoded[i + j] === "=") { + continue; + } + if ( + padding === DecodingPadding.Ignore && + (i + j >= encoded.length || encoded[i + j] === "=") + ) { + continue; + } + if (j > 0 && encoded[i + j - 1] === "=") { + throw new Error("Invalid padding"); + } + if (!(encoded[i + j] in decodeMap)) { + throw new Error("Invalid character"); + } + chunk |= decodeMap[encoded[i + j]] << ((3 - j) * 6); + bitsRead += 6; + } + if (bitsRead < 24) { + let unused: number; + if (bitsRead === 12) { + unused = chunk & 0xffff; + } else if (bitsRead === 18) { + unused = chunk & 0xff; + } else { + throw new Error("Invalid padding"); + } + if (unused !== 0) { + throw new Error("Invalid padding"); + } + } + const byteLength = Math.floor(bitsRead / 8); + for (let i = 0; i < byteLength; i++) { + result[totalBytes] = (chunk >> (16 - i * 8)) & 0xff; + totalBytes++; + } + } + return result.slice(0, totalBytes); +} + +enum EncodingPadding { + Include = 0, + None, +} + +enum DecodingPadding { + Required = 0, + Ignore, +} + +const base64DecodeMap = { + "0": 52, + "1": 53, + "2": 54, + "3": 55, + "4": 56, + "5": 57, + "6": 58, + "7": 59, + "8": 60, + "9": 61, + A: 0, + B: 1, + C: 2, + D: 3, + E: 4, + F: 5, + G: 6, + H: 7, + I: 8, + J: 9, + K: 10, + L: 11, + M: 12, + N: 13, + O: 14, + P: 15, + Q: 16, + R: 17, + S: 18, + T: 19, + U: 20, + V: 21, + W: 22, + X: 23, + Y: 24, + Z: 25, + a: 26, + b: 27, + c: 28, + d: 29, + e: 30, + f: 31, + g: 32, + h: 33, + i: 34, + j: 35, + k: 36, + l: 37, + m: 38, + n: 39, + o: 40, + p: 41, + q: 42, + r: 43, + s: 44, + t: 45, + u: 46, + v: 47, + w: 48, + x: 49, + y: 50, + z: 51, + "+": 62, + "/": 63, +}; + +const base64urlDecodeMap = { + "0": 52, + "1": 53, + "2": 54, + "3": 55, + "4": 56, + "5": 57, + "6": 58, + "7": 59, + "8": 60, + "9": 61, + A: 0, + B: 1, + C: 2, + D: 3, + E: 4, + F: 5, + G: 6, + H: 7, + I: 8, + J: 9, + K: 10, + L: 11, + M: 12, + N: 13, + O: 14, + P: 15, + Q: 16, + R: 17, + S: 18, + T: 19, + U: 20, + V: 21, + W: 22, + X: 23, + Y: 24, + Z: 25, + a: 26, + b: 27, + c: 28, + d: 29, + e: 30, + f: 31, + g: 32, + h: 33, + i: 34, + j: 35, + k: 36, + l: 37, + m: 38, + n: 39, + o: 40, + p: 41, + q: 42, + r: 43, + s: 44, + t: 45, + u: 46, + v: 47, + w: 48, + x: 49, + y: 50, + z: 51, + "-": 62, + _: 63, +};