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.
This commit is contained in:
The Winter Soldier 2025-07-28 23:07:51 -07:00
parent 314a14a45b
commit 0470319e54
2 changed files with 104 additions and 42 deletions

View File

@ -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;
}

View File

@ -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);
}
}