mirror of
https://github.com/better-auth/better-auth.git
synced 2026-06-01 11:56:43 -05:00
chore: add base 64 utility
This commit is contained in:
77
packages/better-auth/src/crypto/base64.test.ts
Normal file
77
packages/better-auth/src/crypto/base64.test.ts
Normal file
@@ -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();
|
||||
});
|
||||
322
packages/better-auth/src/crypto/base64.ts
Normal file
322
packages/better-auth/src/crypto/base64.ts
Normal file
@@ -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<string, number>,
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user