1 import { Secret } from 'otpauth';
3 export const B32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
5 const base32ToBuf = (str: string) => {
7 while (str[end - 1] === '=') --end;
9 const cstr = (end < str.length ? str.substring(0, end) : str).toUpperCase();
10 if (cstr.length === 0) throw new TypeError('Empty secret');
12 /** PATCH: Ensure that the buffer size is at least 1 byte even for single-character inputs.
13 * Base32 encoding and decoding involve packing and unpacking groups of characters into bytes.
14 * In the original `otpauth` lib, the buffer size ((cstr.length * 5) / 8) | 0) might result in 0
15 * for inputs with fewer than 8 bits. However, the minimum buffer size required for any data
16 * is 1 byte. Therefore, we use Math.max(1, ...) to ensure a minimum buffer size of 1 byte.
17 * This guarantees that the buffer has sufficient capacity for at least one byte, even when
18 * processing single-character inputs, preventing issues with zero-sized buffers.
19 * see: `https://github.com/hectorm/otpauth/blob/acbfad3f9492adb5bfa8c2cca1371e880aaa0400/src/utils/encoding/base32.js#L47` */
20 const bufSize = Math.max(1, ((cstr.length * 5) / 8) | 0);
21 const buf = new ArrayBuffer(bufSize);
23 const arr = new Uint8Array(buf);
29 for (let i = 0; i < cstr.length; i++) {
30 const idx = B32_ALPHABET.indexOf(cstr[i]);
31 if (idx === -1) throw new TypeError(`Invalid character found: ${cstr[i]}`);
33 value = (value << 5) | idx;
38 arr[index++] = value >>> bits;
42 /* At this stage we may have remaining bits to process but depending
43 * on the base32 implementation this could lead to different results.
44 * Pass mobile apps are not handling these trailing bits. */
50 * Extends the base `otpauth` class `Secret` with a patch to handle base32 encoding edge-cases.
51 * This patch is designed to retain the initial base32 secret when rebuilding the OTP URL.
52 * It ensures that a single character, such as 'A', is not unintentionally converted to 'AA'
53 * due to missing bits during encoding and decoding processes.
55 export class PatchedSecret extends Secret {
56 private initialBase32Secret: string | undefined;
58 public setInitialBase32Secret(str: string) {
59 this.initialBase32Secret = str.toUpperCase();
63 return this.initialBase32Secret ?? super.base32;
67 Secret.fromBase32 = (str: string) => {
68 const instance = new PatchedSecret({ buffer: base32ToBuf(str) });
69 instance.setInitialBase32Secret(str);