Skip to content

Commit

Permalink
feat: add base32 encode
Browse files Browse the repository at this point in the history
  • Loading branch information
Bekacru committed Dec 23, 2024
1 parent 7bc3d91 commit 88005c7
Show file tree
Hide file tree
Showing 9 changed files with 407 additions and 226 deletions.
6 changes: 2 additions & 4 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@
}
},
"files": {
"ignore": [
"dist"
]
"ignore": ["dist"]
}
}
}
160 changes: 79 additions & 81 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,82 +1,80 @@
{
"name": "@better-auth/utils",
"version": "0.2.1",
"license": "MIT",
"description": "A collection of utilities for better-auth",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"scripts": {
"test": "vitest",
"typecheck": "tsc --noEmit",
"build": "unbuild",
"lint:fix": "biome check . --write"
},
"keywords": [
"auth",
"utils",
"typescript",
"better-auth",
"better-auth-utils"
],
"author": "Bereket Engida",
"repository": {
"type": "git",
"url": "https://github.com/better-auth/utils"
},
"dependencies": {
"uncrypto": "^0.1.3"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/node": "^22.10.1",
"bumpp": "^9.9.0",
"happy-dom": "^15.11.7",
"unbuild": "^2.0.0",
"vitest": "^2.1.8"
},
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./base64": {
"import": "./dist/base64.mjs",
"require": "./dist/base64.cjs"
},
"./binary": {
"import": "./dist/binary.mjs",
"require": "./dist/binary.cjs"
},
"./hash": {
"import": "./dist/hash.mjs",
"require": "./dist/hash.cjs"
},
"./ecdsa": {
"import": "./dist/ecdsa.mjs",
"require": "./dist/ecdsa.cjs"
},
"./hex": {
"import": "./dist/hex.mjs",
"require": "./dist/hex.cjs"
},
"./hmac": {
"import": "./dist/hmac.mjs",
"require": "./dist/hmac.cjs"
},
"./otp": {
"import": "./dist/otp.mjs",
"require": "./dist/otp.cjs"
},
"./random": {
"import": "./dist/random.mjs",
"require": "./dist/random.cjs"
},
"./rsa": {
"import": "./dist/rsa.mjs",
"require": "./dist/rsa.cjs"
}
},
"files": [
"dist"
]
}
"name": "@better-auth/utils",
"version": "0.2.1",
"license": "MIT",
"description": "A collection of utilities for better-auth",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"scripts": {
"test": "vitest",
"typecheck": "tsc --noEmit",
"build": "unbuild",
"lint:fix": "biome check . --write"
},
"keywords": [
"auth",
"utils",
"typescript",
"better-auth",
"better-auth-utils"
],
"author": "Bereket Engida",
"repository": {
"type": "git",
"url": "https://github.com/better-auth/utils"
},
"dependencies": {
"uncrypto": "^0.1.3"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/node": "^22.10.1",
"bumpp": "^9.9.0",
"happy-dom": "^15.11.7",
"unbuild": "^2.0.0",
"vitest": "^2.1.8"
},
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./base64": {
"import": "./dist/base64.mjs",
"require": "./dist/base64.cjs"
},
"./binary": {
"import": "./dist/binary.mjs",
"require": "./dist/binary.cjs"
},
"./hash": {
"import": "./dist/hash.mjs",
"require": "./dist/hash.cjs"
},
"./ecdsa": {
"import": "./dist/ecdsa.mjs",
"require": "./dist/ecdsa.cjs"
},
"./hex": {
"import": "./dist/hex.mjs",
"require": "./dist/hex.cjs"
},
"./hmac": {
"import": "./dist/hmac.mjs",
"require": "./dist/hmac.cjs"
},
"./otp": {
"import": "./dist/otp.mjs",
"require": "./dist/otp.cjs"
},
"./random": {
"import": "./dist/random.mjs",
"require": "./dist/random.cjs"
},
"./rsa": {
"import": "./dist/rsa.mjs",
"require": "./dist/rsa.cjs"
}
},
"files": ["dist"]
}
193 changes: 193 additions & 0 deletions src/base32.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
//inspired by oslo implementation by pilcrowonpaper: https://github.com/pilcrowonpaper/oslo/blob/main/src/encoding/base32.ts

import type { TypedArray } from "./type";

/**
* Returns the Base32 alphabet based on the encoding type.
* @param hex - Whether to use the hexadecimal Base32 alphabet.
* @returns The appropriate Base32 alphabet.
*/
function getAlphabet(hex: boolean): string {
return hex
? "0123456789ABCDEFGHIJKLMNOPQRSTUV"
: "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
}

/**
* Creates a decode map for the given alphabet.
* @param alphabet - The Base32 alphabet.
* @returns A map of characters to their corresponding values.
*/
function createDecodeMap(alphabet: string): Map<string, number> {
const decodeMap = new Map<string, number>();
for (let i = 0; i < alphabet.length; i++) {
decodeMap.set(alphabet[i]!, i);
}
return decodeMap;
}

/**
* Encodes a Uint8Array into a Base32 string.
* @param data - The data to encode.
* @param alphabet - The Base32 alphabet to use.
* @param padding - Whether to include padding.
* @returns The Base32 encoded string.
*/
function base32Encode(
data: Uint8Array,
alphabet: string,
padding: boolean,
): string {
let result = "";
let buffer = 0;
let shift = 0;

for (const byte of data) {
buffer = (buffer << 8) | byte;
shift += 8;
while (shift >= 5) {
shift -= 5;
result += alphabet[(buffer >> shift) & 0x1f];
}
}

if (shift > 0) {
result += alphabet[(buffer << (5 - shift)) & 0x1f];
}

if (padding) {
const padCount = (8 - (result.length % 8)) % 8;
result += "=".repeat(padCount);
}

return result;
}

/**
* Decodes a Base32 string into a Uint8Array.
* @param data - The Base32 encoded string.
* @param alphabet - The Base32 alphabet to use.
* @returns The decoded Uint8Array.
*/
function base32Decode(data: string, alphabet: string): Uint8Array {
const decodeMap = createDecodeMap(alphabet);
const result: number[] = [];
let buffer = 0;
let bitsCollected = 0;

for (const char of data) {
if (char === "=") break;
const value = decodeMap.get(char);
if (value === undefined) {
throw new Error(`Invalid Base32 character: ${char}`);
}
buffer = (buffer << 5) | value;
bitsCollected += 5;

while (bitsCollected >= 8) {
bitsCollected -= 8;
result.push((buffer >> bitsCollected) & 0xff);
}
}

return Uint8Array.from(result);
}

/**
* Base32 encoding and decoding utility.
*/
export const base32 = {
/**
* Encodes data into a Base32 string.
* @param data - The data to encode (ArrayBuffer, TypedArray, or string).
* @param options - Encoding options.
* @returns The Base32 encoded string.
*/
encode(
data: ArrayBuffer | TypedArray | string,
options: { padding?: boolean } = {},
): string {
const alphabet = getAlphabet(false);
const buffer =
typeof data === "string"
? new TextEncoder().encode(data)
: new Uint8Array(data);
return base32Encode(buffer, alphabet, options.padding ?? true);
},

/**
* Decodes a Base32 string into a Uint8Array.
* @param data - The Base32 encoded string or ArrayBuffer/TypedArray.
* @returns The decoded Uint8Array.
*/
decode(data: string | ArrayBuffer | TypedArray): Uint8Array {
if (typeof data !== "string") {
data = new TextDecoder().decode(data);
}
const alphabet = getAlphabet(false);
return base32Decode(data, alphabet);
},
};

/**
* Base32hex encoding and decoding utility.
*/
export const base32hex = {
/**
* Encodes data into a Base32hex string.
* @param data - The data to encode (ArrayBuffer, TypedArray, or string).
* @param options - Encoding options.
* @returns The Base32hex encoded string.
*/
encode(
data: ArrayBuffer | TypedArray | string,
options: { padding?: boolean } = {},
): string {
const alphabet = getAlphabet(true);
const buffer =
typeof data === "string"
? new TextEncoder().encode(data)
: new Uint8Array(data);
return base32Encode(buffer, alphabet, options.padding ?? true);
},

/**
* Decodes a Base32hex string into a Uint8Array.
* @param data - The Base32hex encoded string.
* @returns The decoded Uint8Array.
*/
decode(data: string): Uint8Array {
const alphabet = getAlphabet(true);
return base32Decode(data, alphabet);
},
};

/** @deprecated Use `base32.encode()` instead */
export function encodeBase32(
data: ArrayBuffer | TypedArray,
options?: { padding?: boolean },
): string {
return base32.encode(data, {
padding: options?.padding ?? true,
});
}

/** @deprecated Use `base32.decode()` instead */
export function decodeBase32(data: string): Uint8Array {
return base32.decode(data);
}

/** @deprecated Use `base32hex.encode()` instead */
export function encodeBase32hex(
data: ArrayBuffer | TypedArray,
options?: { padding?: boolean },
): string {
return base32hex.encode(data, {
padding: options?.padding ?? true,
});
}

/** @deprecated Use `base32hex.decode()` instead */
export function decodeBase32hex(data: string): Uint8Array {
return base32hex.decode(data);
}
8 changes: 6 additions & 2 deletions src/hash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,18 @@ describe("digest", () => {
});

it("handles input as an ArrayBufferView", async () => {
const hash = await createHash("SHA-256").digest(new Uint8Array(inputBuffer));
const hash = await createHash("SHA-256").digest(
new Uint8Array(inputBuffer),
);
expect(hash).toBeInstanceOf(ArrayBuffer);
});
});

describe("Error handling", () => {
it("throws an error for unsupported hash algorithms", async () => {
await expect(createHash("SHA-10" as any).digest(inputString)).rejects.toThrow();
await expect(
createHash("SHA-10" as any).digest(inputString),
).rejects.toThrow();
});

it("throws an error for invalid input types", async () => {
Expand Down
Loading

0 comments on commit 88005c7

Please sign in to comment.