Update selected item color in Pass menu
[ProtonMail-WebClient.git] / packages / pass / lib / otp / patch.ts
blob21929a82ef4356870f990b50d6bd46c12f1fdabc
1 import { Secret } from 'otpauth';
3 export const B32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
5 const base32ToBuf = (str: string) => {
6     let end = str.length;
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);
25     let bits = 0;
26     let value = 0;
27     let index = 0;
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;
34         bits += 5;
36         if (bits >= 8) {
37             bits -= 8;
38             arr[index++] = value >>> bits;
39         }
40     }
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. */
46     return buf;
49 /**
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.
54  */
55 export class PatchedSecret extends Secret {
56     private initialBase32Secret: string | undefined;
58     public setInitialBase32Secret(str: string) {
59         this.initialBase32Secret = str.toUpperCase();
60     }
62     get base32() {
63         return this.initialBase32Secret ?? super.base32;
64     }
67 Secret.fromBase32 = (str: string) => {
68     const instance = new PatchedSecret({ buffer: base32ToBuf(str) });
69     instance.setInitialBase32Secret(str);
70     return instance;