From a34fe89ecfd2fc8956d7b006ca7b61836fa6d6f4 Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Fri, 19 Jun 2026 04:49:11 -0700 Subject: [PATCH] fix(tools): validate base encoding edge cases --- .../tools/base-encoding-converter/utils.ts | 56 ++++++++++++++----- tests/unit/base-encoding-utils.test.ts | 11 ++++ 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/features/tools/base-encoding-converter/utils.ts b/src/features/tools/base-encoding-converter/utils.ts index ae1ee607..cb9e0485 100644 --- a/src/features/tools/base-encoding-converter/utils.ts +++ b/src/features/tools/base-encoding-converter/utils.ts @@ -49,6 +49,24 @@ export function decodeBase32ToBytes(value: string): Uint8Array { } const unpadded = normalized.replace(/=+$/g, "") + const paddingLength = normalized.length - unpadded.length + const unpaddedRemainder = unpadded.length % 8 + const expectedPaddingByRemainder: Partial> = { + 0: 0, + 2: 6, + 4: 4, + 5: 3, + 7: 1, + } + const expectedPaddingLength = expectedPaddingByRemainder[unpaddedRemainder] + + if (expectedPaddingLength === undefined) { + throw new Error("Invalid Base32 character.") + } + if (paddingLength > 0 && (normalized.length % 8 !== 0 || paddingLength !== expectedPaddingLength)) { + throw new Error("Invalid Base32 character.") + } + const bytes: number[] = [] let buffer = 0 let bitsLeft = 0 @@ -68,14 +86,23 @@ export function decodeBase32ToBytes(value: string): Uint8Array { } } + if (bitsLeft > 0 && (buffer & ((1 << bitsLeft) - 1)) !== 0) { + throw new Error("Invalid Base32 character.") + } + return new Uint8Array(bytes) } export function encodeBytesToBase58(bytes: Uint8Array): string { if (bytes.length === 0) return "" - const digits = [0] - for (const byte of bytes) { + let leadingZeroCount = 0 + while (leadingZeroCount < bytes.length && bytes[leadingZeroCount] === 0) { + leadingZeroCount += 1 + } + + const digits: number[] = [] + for (const byte of bytes.slice(leadingZeroCount)) { let carry = byte for (let index = 0; index < digits.length; index += 1) { carry += digits[index] << 8 @@ -88,20 +115,21 @@ export function encodeBytesToBase58(bytes: Uint8Array): string { } } - for (const byte of bytes) { - if (byte !== 0) break - digits.push(0) - } - - return digits.reverse().map((digit) => BASE58_ALPHABET[digit]).join("") + const encoded = digits.reverse().map((digit) => BASE58_ALPHABET[digit]).join("") + return `${BASE58_ALPHABET[0].repeat(leadingZeroCount)}${encoded}` } export function decodeBase58ToBytes(value: string): Uint8Array { const normalized = value.trim().replace(/\s+/g, "") if (!normalized) return new Uint8Array() - const bytes = [0] - for (const char of normalized) { + let leadingZeroCount = 0 + while (leadingZeroCount < normalized.length && normalized[leadingZeroCount] === BASE58_ALPHABET[0]) { + leadingZeroCount += 1 + } + + const bytes: number[] = [] + for (const char of normalized.slice(leadingZeroCount)) { const digit = BASE58_ALPHABET.indexOf(char) if (digit < 0) { throw new Error("Invalid Base58 character.") @@ -118,12 +146,10 @@ export function decodeBase58ToBytes(value: string): Uint8Array { } } - for (const char of normalized) { - if (char !== BASE58_ALPHABET[0]) break - bytes.push(0) - } + const decoded = bytes.reverse() + decoded.unshift(...Array.from({ length: leadingZeroCount }, () => 0)) - return new Uint8Array(bytes.reverse()) + return new Uint8Array(decoded) } export function encodeTextToBase32(value: string): string { diff --git a/tests/unit/base-encoding-utils.test.ts b/tests/unit/base-encoding-utils.test.ts index 4dcd950e..a37da10c 100644 --- a/tests/unit/base-encoding-utils.test.ts +++ b/tests/unit/base-encoding-utils.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest" import { + decodeBase32ToBytes, decodeBase58ToBytes, decodeBase32ToText, decodeBase58ToText, @@ -21,6 +22,10 @@ describe("base encoding converter utils", () => { it("encodes Bitcoin Base58 and preserves leading zero bytes", () => { expect(encodeTextToBase58("Hello World!")).toBe("2NEpo7TZRRrLZSi2U") + expect(encodeBytesToBase58(new Uint8Array([0]))).toBe("1") + expect(encodeBytesToBase58(new Uint8Array([0, 0]))).toBe("11") + expect(Array.from(decodeBase58ToBytes("1"))).toEqual([0]) + expect(Array.from(decodeBase58ToBytes("11"))).toEqual([0, 0]) expect(encodeBytesToBase58(new Uint8Array([0, 0, 1]))).toBe("112") expect(Array.from(decodeBase58ToBytes("112"))).toEqual([0, 0, 1]) }) @@ -34,4 +39,10 @@ describe("base encoding converter utils", () => { it("rejects ambiguous Base58 characters", () => { expect(() => decodeBase58ToText("0OIl")).toThrow("Invalid Base58 character.") }) + + it("rejects malformed Base32 padding and impossible lengths", () => { + for (const value of ["A", "AAA", "MZ======", "MZXW6YTBOI="]) { + expect(() => decodeBase32ToBytes(value)).toThrow("Invalid Base32 character.") + } + }) })