Skip to content

Instantly share code, notes, and snippets.

@bellbind
Last active April 27, 2023 16:22
Show Gist options
  • Save bellbind/871b145110c458e83077a718aef9fa0e to your computer and use it in GitHub Desktop.
Save bellbind/871b145110c458e83077a718aef9fa0e to your computer and use it in GitHub Desktop.
[ECMAScript][es6]base64 and base32 encode/decode Uint8Array <=> string
// base32 elements
//RFC4648: why include 2? Z and 2 looks similar than 8 and O
const b32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
console.assert(b32.length === 32, b32.length);
const b32r = new Map(Array.from(b32, (ch, i) => [ch, i])).set("=", 0);
//[constants derived from character table size]
//cbit = 5 (as 32 == 2 ** 5), ubit = 8 (as byte)
//ccount = 8 (= cbit / gcd(cbit, ubit)), ucount = 5 (= ubit / gcd(cbit, ubit))
//cmask = 0x1f (= 2 ** cbit - 1), umask = 0xff (= 2 ** ubit - 1)
//const b32pad = [0, 6, 4, 3, 1];
const b32pad = Array.from(Array(5), (_, i) => (8 - i * 8 / 5 | 0) % 8);
function b32e5(u1, u2 = 0, u3 = 0, u4 = 0, u5 = 0) {
const u40 = u1 * 2 ** 32 + u2 * 2 ** 24 + u3 * 2 ** 16 + u4 * 2 ** 8 + u5;
return [b32[u40 / 2 ** 35 & 0x1f], b32[u40 / 2 ** 30 & 0x1f],
b32[u40 / 2 ** 25 & 0x1f], b32[u40 / 2 ** 20 & 0x1f],
b32[u40 / 2 ** 15 & 0x1f], b32[u40 / 2 ** 10 & 0x1f],
b32[u40 / 2 ** 5 & 0x1f], b32[u40 & 0x1f]];
}
function b32d8(b1, b2, b3, b4, b5, b6, b7, b8) {
const u40 = b32r.get(b1) * 2 ** 35 + b32r.get(b2) * 2 ** 30 +
b32r.get(b3) * 2 ** 25 + b32r.get(b4) * 2 ** 20 +
b32r.get(b5) * 2 ** 15 + b32r.get(b6) * 2 ** 10 +
b32r.get(b7) * 2 ** 5 + b32r.get(b8);
return [u40 / 2 ** 32 & 0xff, u40 / 2 ** 24 & 0xff, u40 / 2 ** 16 & 0xff,
u40 / 2 ** 8 & 0xff, u40 & 0xff];
}
// base32 encode/decode: Uint8Array <=> string
function b32e(u8a) {
console.assert(u8a instanceof Uint8Array, u8a.constructor);
const len = u8a.length, rem = len % 5;
const u5s = Array.from(Array((len - rem) / 5),
(_, i) => u8a.subarray(i * 5, i * 5 + 5));
const pad = b32pad[rem];
const br = rem === 0 ? [] : b32e5(...u8a.subarray(-rem)).slice(0, 8 - pad);
return [].concat(...u5s.map(u5 => b32e5(...u5)),
br, ["=".repeat(pad)]).join("");
}
function b32d(bs) {
const len = bs.length;
if (len === 0) return new Uint8Array([]);
console.assert(len % 8 === 0, len);
const pad = len - bs.indexOf("="), rem = b32pad.indexOf(pad);
console.assert(rem >= 0, pad);
console.assert(/^[A-Z2-7+\/]*$/.test(bs.slice(0, len - pad)), bs);
const u8s = [].concat(...bs.match(/.{8}/g).map(b8 => b32d8(...b8)));
return new Uint8Array(rem > 0 ? u8s.slice(0, rem - 5) : u8s);
}
if (typeof require === "function" && require.main === module) {
//Check encode <=> decode
const buf = require("crypto").randomBytes(32);
//NOTE: basic test case
//const buf = Buffer.from([0xf0, 0xff, 0x0f, 0xf0, 0xff]); //`6D7Q74H7`
//const buf = Buffer.from([0xf0, 0xff, 0x0f, 0xf0]); //`6D7Q74A=`
//const buf = Buffer.from([0xf0, 0xff, 0x0f]); //`6D7Q6===`
//const buf = Buffer.from([0x5b, 0xb5]); // `LO2Q====`
//const buf = Buffer.from([0xb5]); // `WU======`
//const buf = Buffer.alloc(0);
const u8a = new Uint8Array(buf);
// compare base32 encode
console.log(b32e(u8a));
// compare base32 decode
console.log(Array.from(b32d(b32e(u8a))).every((u, i) => u === u8a[i]));
//console.log(b32d(b32e(u8a)));
}
// base64 elements
const b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
console.assert(b64.length === 64, b64.length);
const b64r = new Map(Array.from(b64, (ch, i) => [ch, i])).set("=", 0);
//[constants derived from character table size]
//cbit = 6 (as 64 == 2 ** 6), ubit = 8 (as byte)
//ccount = 3 (= cbit / gcd(cbit, ubit)), ucount = 4 (= ubit / gcd(cbit, ubit))
//cmask = 0x3f (= 2 ** cbit - 1), umask = 0xff (= 2 ** ubit - 1)
//const b64pad = [0, 2, 1];
const b64pad = Array.from(Array(3), (_, i) => (4 - i * 8 / 6 | 0) % 4);
function b64e3(u1, u2 = 0, u3 = 0) {
const u24 = u1 << 16 | u2 << 8 | u3;
return [b64[u24 >>> 18], b64[u24 >>> 12 & 0x3f],
b64[u24 >>> 6 & 0x3f], b64[u24 & 0x3f]];
}
function b64d4(b1, b2, b3, b4) {
const u24 = b64r.get(b1) << 18 | b64r.get(b2) << 12 |
b64r.get(b3) << 6 | b64r.get(b4);
return [u24 >>> 16, u24 >>> 8 & 0xff, u24 & 0xff];
}
// base64 encode/decode: Uint8Array <=> string
function b64e(u8a) {
console.assert(u8a instanceof Uint8Array, u8a.constructor);
const len = u8a.length, rem = len % 3;
const u3s = Array.from(Array((len - rem) / 3),
(_, i) => u8a.subarray(i * 3, i * 3 + 3));
const pad = b64pad[rem];
const br = rem === 0 ? [] : b64e3(...u8a.subarray(-rem)).slice(0, 4 - pad);
return [].concat(...u3s.map(u3 => b64e3(...u3)),
br, ["=".repeat(pad)]).join("");
}
function b64d(bs) {
const len = bs.length;
if (len === 0) return new Uint8Array([]);
//NOTE: allow strict base64 string: no ignore invalids and no supplements
console.assert(len % 4 === 0, len);
const pad = len - bs.indexOf("="), rem = b64pad.indexOf(pad);
console.assert(rem >= 0, pad);
console.assert(/^[A-Za-z0-9+\/]*$/.test(bs.slice(0, len - rem)), bs);
const u8s = [].concat(...bs.match(/.{4}/g).map(b4 => b64d4(...b4)));
return new Uint8Array(rem > 0 ? u8s.slice(0, rem - 3) : u8s);
}
if (typeof require === "function" && require.main === module) {
//Check with comparing nodejs Buffer.toString("base64")
const buf = require("crypto").randomBytes(32);
//NOTE: basic test case
//const buf = Buffer.from([0xf0, 0xff, 0x0f]); //`8P8P`
//const buf = Buffer.from([0x5b, 0xb5]); // `W5U=`
//const buf = Buffer.from([0xb5]); // `tQ==`
//const buf = Buffer.alloc(0);
const u8a = new Uint8Array(buf);
// compare base64 encode
console.log(b64e(u8a));
console.log(buf.toString("base64"));
console.log(b64e(u8a) === buf.toString("base64"));
// compare base64 decode
console.log(Array.from(b64d(b64e(u8a))).every((u, i) => u === u8a[i]));
//console.log(b64d(b64e(u8a)));
}
// generic BASE 2**N encoder/decoder
function BasePow2(ctable, cpad = "=") {
const clen = ctable.length, cbit = Math.log2(clen);
console.assert(cpad.length === 1, `cpad must be 1 char, but: ${cpad}`);
console.assert(!ctable.includes(cpad),
`ctable should not include pad char: ${cpad} in ${ctable}`);
console.assert(clen > 0 && Number.isInteger(cbit),
`length of ctable must be power of 2, but: ${clen}`);
const ulen = 256, ubit = 8;
const unitbit = lcm(cbit, ubit);
console.assert(unitbit < Math.log2(Number.MAX_SAFE_INTEGER), unitbit);
const ccount = unitbit / cbit, ucount = unitbit / ubit;
const pads = Array.from(
Array(ucount), (_, i) => (ccount - i * ubit / cbit | 0) % ccount);
const crev = new Map(Array.from(ctable, (ch, i) => [ch, i])).set(cpad, 0);
const regBody = new RegExp(`^[${ctable}]*$`);
const regPad = new RegExp(`^${cpad}*$`);
return {encode, decode};
function lcm(a, b) {return a * b / gcd(a, b);}
function gcd(a, b) {
if (a > b) a %= b;
while (a > 0) [a, b] = [b % a, a];
return b;
}
function unitEncode(u8s) {
//console.assert(u8s.length === ucount, u8s.length, u8s);
let unit = u8s.reduce((s, u8 = 0) => s * ulen + u8, 0);
const result = [];
for (let i = 0; i < ccount; i++) {
const part = unit % clen;
result.unshift(ctable[part]);
unit = (unit - part) / clen;
}
return result;
}
function unitDecode(chs) {
//console.assert(chs.length === ccount, chs.length, chs);
let unit = [...chs].reduce(
(s, ch = cpad) => s * clen + crev.get(ch), 0);
const result = [];
for (let i = 0; i < ucount; i++) {
const part = unit % ulen;
result.unshift(part);
unit = (unit - part) / ulen;
}
return result;
}
function encode(u8s) {
if (!(u8s instanceof Uint8Array)) throw new TypeError(
`args must be a Uint8Array: ${u8s.constructor}`);
const len = u8s.length, rem = len % ucount;
const u8ss = Array.from(
Array((len - rem) / ucount),
(_, i) => u8s.subarray(i * ucount, i * ucount + ucount));
const pad = pads[rem];
let br = [];
if (rem > 0) {
const units = new Uint8Array(ucount);
units.set(u8s.subarray(-rem));
br = unitEncode(units).slice(0, ccount - pad);
}
return [].concat(
...u8ss.map(unitEncode), br, [cpad.repeat(pad)]).join("");
}
function decode(bs) {
if (typeof bs !== "string") throw new TypeError(
`args must be a string: ${typeof bs}`);
const len = bs.length;
if (len === 0) return new Uint8Array([]);
if (len % ccount !== 0) throw new TypeError(
`length of the arg must me multiple of ${ccount}, but: ${len}`);
const pad = len - bs.indexOf(cpad), rem = pads.indexOf(pad);
if (rem < 0) throw new TypeError(
`valid pad length is ${pads}, but: ${pad}`);
if (!regBody.test(bs.slice(0, len - pad))) throw new TypeError(
`invalid chars included in: "${bs.slice(0, len - pad)}"`);
if (!regPad.test(bs.slice(-pad))) throw new TypeError(
`pad must consist only "${cpad}", but "${bs.slice(-pad)}"`);
const u8s = [].concat(...Array.from(
Array(len / ccount),
(_, i) => unitDecode(bs.slice(i * ccount, i * ccount + ccount))));
return new Uint8Array(rem > 0 ? u8s.slice(0, rem - ucount) : u8s);
}
}
if (typeof require === "function" && require.main === module) {
const b64 = BasePow2(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
//const b32 = BasePow2("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567");
//Check with comparing nodejs Buffer.toString("base64")
const buf = require("crypto").randomBytes(32);
//NOTE: basic test case
//const buf = Buffer.from([0xf0, 0xff, 0x0f]); //`8P8P`
//const buf = Buffer.from([0x5b, 0xb5]); // `W5U=`
//const buf = Buffer.from([0xb5]); // `tQ==`
//const buf = Buffer.alloc(0);
const u8a = new Uint8Array(buf);
// compare base64 encode
console.log(b64.encode(u8a));
console.log(buf.toString("base64"));
console.log(b64.encode(u8a) === buf.toString("base64"));
// compare base64 decode
console.log(Array.from(b64.decode(b64.encode(u8a))).every(
(u, i) => u === u8a[i]));
//console.log(b64.decode(b64.encode(u8a)));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment