From 0470319e54246332e6c64971fdc5c555b8bd78e5 Mon Sep 17 00:00:00 2001 From: The Winter Soldier Date: Mon, 28 Jul 2025 23:07:51 -0700 Subject: [PATCH 1/2] Fixes #2079: Update Base32 operation to support strict mode and no padding The Base64 operation allows for a strict mode, and also allows for removing the padding character from the alphabet. Base32 on the other hand is not aligned with this design. This change updates the Base32 operation to include a strict mode which enforces the same rules as the Base64 tool (adhering to Base32 expectations instead of Base64, such as a maximum of 6 padding characters). In non-strict mode, this allows the user to enter inputs that are not divisible by 8 while still producing an output, such as inputting "GE" to get the output "1" with alphabet "A-Z2-7" (no padding characer). All unit tests for Base32 continue to pass after this change. --- src/core/lib/Base32.mjs | 96 ++++++++++++++++++++++++++++++ src/core/operations/FromBase32.mjs | 50 +++------------- 2 files changed, 104 insertions(+), 42 deletions(-) diff --git a/src/core/lib/Base32.mjs b/src/core/lib/Base32.mjs index 92b76eca7..002a51662 100644 --- a/src/core/lib/Base32.mjs +++ b/src/core/lib/Base32.mjs @@ -1,5 +1,8 @@ // import Utils from "../Utils.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import Utils from "../Utils.mjs"; + /** * Base32 resources. * @@ -21,3 +24,96 @@ export const ALPHABET_OPTIONS = [ }, ]; +/** + * Decode the Base32 input string using the given alphabet, returning a byte array. + * @param {*} data + * @param {string} [alphabet="A-Z0-7="] + * @param {*} [returnType="string"] - Either "string" or "byteArray" + * @param {boolean} [removeNonAlphChars=true] + * @returns {byteArray} + * + * @example + * // returns "Hello" + * fromBase32("JBSWY3DP") + * + * // returns [72, 101, 108, 108, 111] + * fromBase32("JBSWY3DP", null, "byteArray") + */ +export function fromBase32(data, alphabet="A-Z0-7=", returnType="string", removeNonAlphChars=true, strictMode=false) { + if (!data) { + return returnType === "string" ? "" : []; + } + + alphabet = alphabet || ALPHABET_OPTIONS[0].value; + alphabet = Utils.expandAlphRange(alphabet).join(""); + + // Confirm alphabet is a valid length + if (alphabet.length !== 32 && alphabet.length !== 33) { // Allow for padding + throw new OperationError(`Error: Base32 alphabet should be 32 characters long, or 33 with a padding character. Found ${alphabet.length}: ${alphabet}`); + } + + // Remove non-alphabet characters + if (removeNonAlphChars) { + const re = new RegExp("[^" + alphabet.replace(/[[\]\\\-^$]/g, "\\$&") + "]", "g"); + data = data.replace(re, ""); + } + + if (strictMode) { + // Check for incorrect lengths (even without padding) + if (data.length % 8 === 1) { + throw new OperationError(`Error: Invalid Base32 input length (${data.length}). Cannot be 5n+1, even without padding chars.`); + } + + if (alphabet.length === 33) { // Padding character included + const pad = alphabet.charAt(32); + const padPos = data.indexOf(pad); + if (padPos >= 0) { + // Check that the padding character is only used at the end and maximum of 6 times + if (padPos < data.length - 6 || data.charAt(data.length - 1) !== pad) { + throw new OperationError(`Error: Base32 padding character (${pad}) not used in the correct place.`); + } + + // Check that the input is padded to the correct length + if (data.length % 8 !== 0) { + throw new OperationError("Error: Base32 not padded to a multiple of 8."); + } + } + } + } + + const output = []; + + let chr1, chr2, chr3, chr4, chr5, + enc1, enc2, enc3, enc4, enc5, + enc6, enc7, enc8, i = 0; + + while (i < data.length) { + // Including `|| null` forces empty strings to null so that indexOf returns -1 instead of 0 + enc1 = alphabet.indexOf(data.charAt(i++)); + enc2 = alphabet.indexOf(data.charAt(i++) || null); + enc3 = alphabet.indexOf(data.charAt(i++) || null); + enc4 = alphabet.indexOf(data.charAt(i++) || null); + enc5 = alphabet.indexOf(data.charAt(i++) || null); + enc6 = alphabet.indexOf(data.charAt(i++) || null); + enc7 = alphabet.indexOf(data.charAt(i++) || null); + enc8 = alphabet.indexOf(data.charAt(i++) || null); + + if (strictMode && [enc1, enc2, enc3, enc4, enc5, enc6, enc7, enc8].some(enc => enc < 0)) { + throw new OperationError("Error: Base32 input contains non-alphabet char(s)"); + } + + chr1 = (enc1 << 3) | (enc2 >> 2); + chr2 = ((enc2 & 3) << 6) | (enc3 << 1) | (enc4 >> 4); + chr3 = ((enc4 & 15) << 4) | (enc5 >> 1); + chr4 = ((enc5 & 1) << 7) | (enc6 << 2) | (enc7 >> 3); + chr5 = ((enc7 & 7) << 5) | enc8; + + if (chr1 >= 0 && chr1 < 256) output.push(chr1); + if (((enc2 & 3) !== 0 || enc3 !== 32) && (chr2 >= 0 && chr2 < 256)) output.push(chr2); + if (((enc4 & 15) !== 0 || enc5 !== 32) && (chr3 >= 0 && chr3 < 256)) output.push(chr3); + if (((enc5 & 1) !== 0 || enc6 !== 32) && (chr4 >= 0 && chr4 < 256)) output.push(chr4); + if (((enc7 & 7) !== 0 || enc8 !== 32) && (chr5 >= 0 && chr5 < 256)) output.push(chr5); + } + + return returnType === "string" ? Utils.byteArrayToUtf8(output) : output; +} diff --git a/src/core/operations/FromBase32.mjs b/src/core/operations/FromBase32.mjs index 8ee0f1f87..6c6f9cb8f 100644 --- a/src/core/operations/FromBase32.mjs +++ b/src/core/operations/FromBase32.mjs @@ -5,8 +5,7 @@ */ import Operation from "../Operation.mjs"; -import Utils from "../Utils.mjs"; -import {ALPHABET_OPTIONS} from "../lib/Base32.mjs"; +import {ALPHABET_OPTIONS, fromBase32} from "../lib/Base32.mjs"; /** @@ -36,6 +35,11 @@ class FromBase32 extends Operation { name: "Remove non-alphabet chars", type: "boolean", value: true + }, + { + name: "Strict mode", + type: "boolean", + value: false } ]; this.checks = [ @@ -58,46 +62,8 @@ class FromBase32 extends Operation { * @returns {byteArray} */ run(input, args) { - if (!input) return []; - - const alphabet = args[0] ? - Utils.expandAlphRange(args[0]).join("") : "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=", - removeNonAlphChars = args[1], - output = []; - - let chr1, chr2, chr3, chr4, chr5, - enc1, enc2, enc3, enc4, enc5, enc6, enc7, enc8, - i = 0; - - if (removeNonAlphChars) { - const re = new RegExp("[^" + alphabet.replace(/[\]\\\-^]/g, "\\$&") + "]", "g"); - input = input.replace(re, ""); - } - - while (i < input.length) { - enc1 = alphabet.indexOf(input.charAt(i++)); - enc2 = alphabet.indexOf(input.charAt(i++) || "="); - enc3 = alphabet.indexOf(input.charAt(i++) || "="); - enc4 = alphabet.indexOf(input.charAt(i++) || "="); - enc5 = alphabet.indexOf(input.charAt(i++) || "="); - enc6 = alphabet.indexOf(input.charAt(i++) || "="); - enc7 = alphabet.indexOf(input.charAt(i++) || "="); - enc8 = alphabet.indexOf(input.charAt(i++) || "="); - - chr1 = (enc1 << 3) | (enc2 >> 2); - chr2 = ((enc2 & 3) << 6) | (enc3 << 1) | (enc4 >> 4); - chr3 = ((enc4 & 15) << 4) | (enc5 >> 1); - chr4 = ((enc5 & 1) << 7) | (enc6 << 2) | (enc7 >> 3); - chr5 = ((enc7 & 7) << 5) | enc8; - - output.push(chr1); - if ((enc2 & 3) !== 0 || enc3 !== 32) output.push(chr2); - if ((enc4 & 15) !== 0 || enc5 !== 32) output.push(chr3); - if ((enc5 & 1) !== 0 || enc6 !== 32) output.push(chr4); - if ((enc7 & 7) !== 0 || enc8 !== 32) output.push(chr5); - } - - return output; + const [alphabet, removeNonAlphChars, strictMode] = args; + return fromBase32(input, alphabet, "byteArray", removeNonAlphChars, strictMode); } } From c2df52264634934f6b91daf9598daa5b5e1b5676 Mon Sep 17 00:00:00 2001 From: The Winter Soldier Date: Tue, 29 Jul 2025 00:08:52 -0700 Subject: [PATCH 2/2] Fixes #2079: Update Base32 operation to support strict mode and no padding The Base64 operation allows for a strict mode, and also allows for removing the padding character from the alphabet. Base32 on the other hand is not aligned with this design. This change updates the Base32 operation to include a strict mode which enforces the same rules as the Base64 tool (adhering to Base32 expectations instead of Base64, such as a maximum of 6 padding characters). In non-strict mode, this allows the user to enter inputs that are not divisible by 8 while still producing an output, such as inputting "GE" to get the output "1" with alphabet "A-Z2-7" (no padding characer). All unit tests for Base32 continue to pass after this change. --- src/core/lib/Base32.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/lib/Base32.mjs b/src/core/lib/Base32.mjs index 002a51662..007b6b32a 100644 --- a/src/core/lib/Base32.mjs +++ b/src/core/lib/Base32.mjs @@ -61,7 +61,7 @@ export function fromBase32(data, alphabet="A-Z0-7=", returnType="string", remove if (strictMode) { // Check for incorrect lengths (even without padding) if (data.length % 8 === 1) { - throw new OperationError(`Error: Invalid Base32 input length (${data.length}). Cannot be 5n+1, even without padding chars.`); + throw new OperationError(`Error: Invalid Base32 input length (${data.length}). Cannot be 8n+1, even without padding chars.`); } if (alphabet.length === 33) { // Padding character included