cesr
Version:
[](https://www.npmjs.com/package/cesr) [](https://github.com/lenkan/cesr-js/blob/main/LICENSE)
465 lines (464 loc) • 16.3 kB
JavaScript
import { encodeBase64Int, encodeBase64Url } from "./encoding-base64.js";
import { decodeBase64Int, decodeBase64Url } from "./encoding-base64.js";
import { IndexCode, IndexTable } from "./codes.js";
import { MatterCode, MatterTable } from "./codes.js";
import { CountCode_10, CountCode_20 } from "./codes.js";
import { decodeUtf8, encodeUtf8 } from "./encoding-utf8.js";
// VERSION = "PPPPVVVKKKKBBBB.";
// LEGACY_VERSION = "PPPPvvKKKKllllll_";
const REGEX_VERSION_STRING_PROTOCOL = /^[A-Z]{4}$/;
const REGEX_VERSION_STRING_KIND = /^[A-Z]{4}$/;
const REGEX_VERSION_JSON = /^\{"v":"(.*?)".*$/;
const REGEX_BASE64_CHARACTER = /^[A-Za-z0-9\-_]+$/;
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const INDEXER_ENCODING_SCHEME = [
{ selector: ALPHABET, size: 1 },
{ selector: "01234", size: 2 },
];
// TODO: Create a lookup table for encoding schemes to avoid looping through
const ENCODING_SCHEME = [
{ selector: ALPHABET, size: 1 },
{ selector: "0456", size: 2 },
{ selector: "123789", size: 4 },
{ selector: "-", type: ALPHABET, size: 2 },
{ selector: "-", type: "-", size: 3 },
{ selector: "-", type: "_", size: 5 },
];
function padNumber(num, length) {
return num.toString().padStart(length, "0");
}
function prepadBytes(raw, length) {
if (raw.byteLength === length) {
return raw;
}
const padded = new Uint8Array(length + raw.byteLength);
padded.set(raw, length);
return padded;
}
function encodeHexInt(value, length) {
if (value >= 16 ** length) {
throw new Error(`value ${value} too big for hex length ${length}`);
}
return value.toString(16).padStart(length, "0");
}
function isIndexer(context) {
if (!context || !context.code.startsWith("-")) {
return false;
}
if (context.version === 1) {
switch (context.code) {
case CountCode_10.ControllerIdxSigs:
case CountCode_10.WitnessIdxSigs:
return true;
default:
return false;
}
}
switch (context.code) {
case CountCode_20.WitnessIdxSigs:
case CountCode_20.BigWitnessIdxSigs:
case CountCode_20.TransIdxSigGroups:
case CountCode_20.BigTransIdxSigGroups:
case CountCode_20.TransLastIdxSigGroups:
case CountCode_20.BigTransLastIdxSigGroups:
case CountCode_20.BigControllerIdxSigs:
case CountCode_20.ControllerIdxSigs:
return true;
default:
return false;
}
}
function findHardSize(input, context) {
const selector = decodeUtf8(input.slice(0, 1));
const type = decodeUtf8(input.slice(1, 2));
if (isIndexer(context)) {
for (const scheme of INDEXER_ENCODING_SCHEME) {
if (scheme.selector.includes(selector)) {
return scheme.size;
}
}
throw new Error(`Invalid first character in input ${selector}`);
}
for (const scheme of ENCODING_SCHEME) {
if (scheme.selector.includes(selector)) {
if (!scheme.type || scheme.type.includes(type)) {
return scheme.size;
}
}
}
throw new Error(`Invalid first character in input ${selector}`);
}
function findCodeSize(hard, context) {
let size = null;
if (isIndexer(context)) {
size = IndexTable[hard];
}
else if (hard.startsWith("-")) {
// Counter, no need to lookup since they are all determined by selector
const selector = hard.charAt(1);
if (ALPHABET.includes(selector)) {
size = { hs: 2, ss: 2, fs: 4 };
}
else if (selector === "-") {
size = { hs: 3, ss: 5, fs: 8 };
}
else if (selector === "_") {
size = { hs: 5, ss: 3, fs: 8 };
}
}
else {
size = MatterTable[hard];
}
if (!size) {
throw new Error(`Unknown code ${hard}`);
}
return size;
}
export function encode(frame, size) {
if (frame.code.length !== size.hs) {
throw new Error(`Frame code ${frame.code} length ${frame.code.length} does not match expected size ${size.hs}`);
}
const ls = size.ls ?? 0;
const ms = (size.ss ?? 0) - (size.os ?? 0);
const os = size.os ?? 0;
const raw = frame.raw ?? new Uint8Array(0);
const padSize = (3 - ((raw.byteLength + ls) % 3)) % 3;
const padded = prepadBytes(raw, padSize + ls);
const soft = ms ? encodeBase64Int(frame.count ?? frame.index ?? padded.byteLength / 3, ms) : "";
const other = os ? encodeBase64Int(frame.ondex ?? 0, os ?? 0) : "";
const result = `${frame.code}${soft}${other}${encodeBase64Url(padded).slice(padSize)}`;
if (size.fs > 0 && result.length < size.fs) {
throw new Error(`Encoded size ${result.length} does not match expected size ${size.fs}`);
}
return result;
}
export function encodeIndexedSignature(alg, raw, index) {
switch (alg) {
case "ed25519":
return encodeIndexer({ code: IndexCode.Ed25519_Sig, raw, index });
default:
throw new Error(`Unsupported signature algorithm: ${alg}`);
}
}
export function encodeMatter(raw) {
const size = MatterTable[raw.code];
if (!size) {
throw new Error(`Unknown matter code ${raw.code}`);
}
return encode(raw, size);
}
export function encodeMap(data) {
const frames = [];
for (const [key, value] of Object.entries(data)) {
frames.push(encodeTag(key));
switch (typeof value) {
case "string":
frames.push(encodeString(value));
break;
case "number":
frames.push(encodeNumber(value));
break;
case "object": {
if (!Array.isArray(value) && !(value instanceof Date)) {
frames.push(encodeMap(value));
}
else {
throw new Error(`Unsupported object type for key ${key}: ${JSON.stringify(value)}`);
}
break;
}
case "boolean":
frames.push(encodeMatter({
code: value ? MatterCode.Yes : MatterCode.No,
raw: new Uint8Array(0),
}));
break;
default:
throw new Error(`Unsupported value type ${typeof value} for key ${key}`);
}
}
const result = frames.join("");
const header = encodeCounter({ code: CountCode_20.GenericMapGroup, count: Math.floor(result.length / 4) });
return `${header}${frames.join("")}`;
}
export function encodeDate(date) {
if (date.toString() === "Invalid Date") {
throw new Error("Invalid date");
}
// TODO: Better design for date encoding
const YYYY = date.getFullYear();
const MM = padNumber(date.getUTCMonth() + 1, 2);
const dd = padNumber(date.getUTCDate(), 2);
const hh = padNumber(date.getUTCHours(), 2);
const mm = padNumber(date.getUTCMinutes(), 2);
const ss = padNumber(date.getUTCSeconds(), 2);
const ms = padNumber(date.getUTCMilliseconds(), 3);
const raw = decodeBase64Url(`${YYYY}-${MM}-${dd}T${hh}c${mm}c${ss}d${ms}000p00c00`);
return encodeMatter({ code: MatterCode.DateTime, raw });
}
export function encodeTag(tag) {
switch (tag.length) {
case 1:
return `${MatterCode.Tag1}${tag.padStart(2, "_")}`;
case 2:
return `${MatterCode.Tag2}${tag}`;
default:
throw new Error(`Could not determine tag size`);
}
}
export function encodeString(txt) {
if (REGEX_BASE64_CHARACTER.test(txt) && !txt.startsWith("A")) {
const textsize = txt.length % 4;
const padsize = (4 - textsize) % 4;
const leadsize = (3 - textsize) % 3;
const raw = decodeBase64Url("A".repeat(padsize) + txt).slice(leadsize);
switch (leadsize) {
case 0:
return encodeMatter({ code: MatterCode.StrB64_L0, raw });
case 1:
return encodeMatter({ code: MatterCode.StrB64_L1, raw });
case 2:
return encodeMatter({ code: MatterCode.StrB64_L2, raw });
default:
throw new Error(`Could not determine lead size`);
}
}
const raw = encodeUtf8(txt);
const length = raw.byteLength;
const leadSize = length % 3;
switch (leadSize) {
case 0:
return encodeMatter({ code: MatterCode.Bytes_L0, raw });
case 1:
return encodeMatter({ code: MatterCode.Bytes_L1, raw });
case 2:
return encodeMatter({ code: MatterCode.Bytes_L2, raw });
default:
throw new Error(`Could not determine lead size`);
}
}
export function encodeInt(value) {
return `${MatterCode.Short}${encodeBase64Int(value, 4)}`;
}
export function encodeNumber(value) {
const txt = value.toString().replace(".", "p");
const textsize = txt.length % 4;
const padsize = (4 - textsize) % 4;
const leadsize = (3 - textsize) % 3;
const raw = decodeBase64Url("A".repeat(padsize) + txt).slice(leadsize);
switch (leadsize) {
case 0:
return encodeMatter({ code: MatterCode.Decimal_L0, raw });
case 1:
return encodeMatter({ code: MatterCode.Decimal_L1, raw });
case 2:
return encodeMatter({ code: MatterCode.Decimal_L2, raw });
default:
throw new Error(`Could not determine lead size for decimal ${value}`);
}
}
export function encodeSignature(alg, raw) {
switch (alg) {
case "ed25519":
return encodeMatter({ code: MatterCode.Ed25519_Sig, raw });
case "secp256k1":
return encodeMatter({ code: MatterCode.ECDSA_256k1_Sig, raw });
default:
throw new Error(`Unsupported signature algorithm: ${alg}`);
}
}
export function encodeDigest(alg, raw) {
switch (alg) {
case "blake3_256":
return encodeMatter({ code: MatterCode.Blake3_256, raw });
case "blake3_512":
return encodeMatter({ code: MatterCode.Blake3_512, raw });
default:
throw new Error(`Unsupported digest algorithm: ${alg}`);
}
}
export function encodeGenus(genus) {
if (typeof genus.major !== "number" || genus.major < 0 || genus.major > 63) {
throw new Error(`Invalid major version: ${genus.major}`);
}
const minor = genus.minor ?? 0;
if (typeof minor !== "number" || minor < 0) {
throw new Error(`Invalid minor version: ${minor}`);
}
const qb64 = `${CountCode_10.KERIACDCGenusVersion}${encodeBase64Int(genus.major, 1)}${encodeBase64Int(minor, 2)}`;
return qb64;
}
export function encodeCounter(raw) {
return encode(raw, findCodeSize(raw.code));
}
export function encodeAttachmentsV1(count) {
if (count > 64 ** 2) {
return encodeCounter({ code: CountCode_10.BigAttachmentGroup, count });
}
return encodeCounter({ code: CountCode_10.AttachmentGroup, count });
}
export function encodeAttachmentsV2(count) {
if (count > 64 ** 2) {
return encodeCounter({ code: CountCode_20.BigAttachmentGroup, count });
}
return encodeCounter({ code: CountCode_20.AttachmentGroup, count });
}
export function encodeIndexer(frame) {
const size = IndexTable[frame.code];
if (!size) {
throw new Error(`Unknown indexer code ${frame.code}`);
}
return encode(frame, size);
}
export function encodeVersionString(init) {
const protocol = init.protocol ?? "KERI";
const major = init.major ?? 1;
const minor = init.minor ?? 0;
const format = init.kind ?? "JSON";
if (format !== "JSON") {
throw new Error("Only JSON format is supported for now");
}
if (!REGEX_VERSION_STRING_PROTOCOL.test(protocol)) {
throw new Error("Protocol must be 4 characters");
}
if (!REGEX_VERSION_STRING_KIND.test(format)) {
throw new Error("Format must be 4 characters");
}
if (init.legacy) {
const version = `${encodeHexInt(major, 1)}${encodeHexInt(minor, 1)}`;
const size = encodeHexInt(init.size ?? 0, 6);
return `${protocol}${version}${format}${size}_`;
}
const version = `${encodeBase64Int(major, 1)}${encodeBase64Int(minor, 2)}`;
const size = encodeBase64Int(init.size ?? 0, 4);
return `${protocol}${version}${format}${size}.`;
}
export function encodeMessage(body, init = {}) {
const str = encodeUtf8(JSON.stringify({
v: encodeVersionString(init),
...body,
}));
return JSON.stringify({
v: encodeVersionString({ ...init, size: str.byteLength }),
...body,
});
}
export function decode(input, context) {
const result = decodeStream(input, context);
if (result.frame === null) {
throw new Error("Not enough data in input");
}
return result.frame;
}
export function decodeStream(input, context) {
if (typeof input === "string") {
input = encodeUtf8(input);
}
if (input.length < 1) {
return { frame: null, n: 0 };
}
const hs = findHardSize(input, context);
if (input.length < hs) {
return { frame: null, n: 0 };
}
const hard = decodeUtf8(input.slice(0, hs));
const size = findCodeSize(hard, context);
if (!size) {
throw new Error(`Unknown code ${hard}`);
}
const cs = size.hs + size.ss;
if (input.length < cs) {
return { frame: null, n: 0 };
}
const ls = size.ls ?? 0;
const ps = (size.hs + size.ss) % 4;
const ms = size.ss - (size.os ?? 0);
const os = size.os ?? 0;
const soft0 = decodeBase64Int(decodeUtf8(input.slice(size.hs, cs)));
const soft1 = decodeBase64Int(decodeUtf8(input.slice(size.hs + ms, size.hs + ms + os)));
const fs = size.fs > 0 ? size.fs : cs + soft0 * 4;
if (input.length < fs) {
return { frame: null, n: 0 };
}
const padding = "A".repeat(ps);
const text = decodeUtf8(input.slice(0, fs));
const rawtext = padding + text.slice(cs, fs);
const raw = decodeBase64Url(rawtext).slice(ps + ls);
return { frame: { code: hard, count: soft0, index: soft0, ondex: soft1, raw, text }, n: fs };
}
export function decodeGenus(input) {
const major = decodeBase64Int(input.slice(5, 6));
const minor = decodeBase64Int(input.slice(6, 8));
return { major, minor };
}
export function decodeMatter(input) {
return decode(input);
}
export function decodeCounter(input) {
return decode(input);
}
export function decodeVersionString(input) {
if (typeof input !== "string") {
input = decodeUtf8(input.slice(0, 24));
}
const match = input.match(REGEX_VERSION_JSON);
if (!match) {
throw new Error(`Unable to extract "v" field from ${input}`);
}
const value = match[1];
if (value.endsWith(".") && value.length === 16) {
const protocol = value.slice(0, 4);
const major = decodeBase64Int(value.slice(4, 5));
const minor = decodeBase64Int(value.slice(5, 7));
const kind = value.slice(7, 11);
const size = decodeBase64Int(value.slice(12, 15));
return {
protocol,
major,
minor,
legacy: false,
kind,
size,
};
}
if (value.endsWith("_") && value.length === 17) {
const protocol = value.slice(0, 4);
const major = parseInt(value.slice(4, 5), 16);
const minor = parseInt(value.slice(5, 6), 16);
const format = value.slice(6, 10);
const size = parseInt(value.slice(10, 16), 16);
return {
protocol,
major,
minor,
kind: format,
size,
legacy: true,
};
}
throw new Error(`Invalid version string ${value}`);
}
export const encoding = {
encode,
encodeIndexedSignature,
encodeMap,
encodeMatter,
encodeDate,
encodeString,
encodeNumber,
encodeSignature,
encodeDigest,
encodeGenus,
encodeCounter,
encodeAttachmentsV1,
encodeAttachmentsV2,
encodeIndexer,
encodeVersionString,
encodeMessage,
decode,
decodeStream,
decodeGenus,
decodeMatter,
decodeCounter,
decodeVersionString,
};