@aokiapp/tlv
Version:
Tag-Length-Value (TLV) parser and builder library with schema support. Provides both parsing and building APIs as submodules.
159 lines (158 loc) • 4.9 kB
JavaScript
/**
* Common encode/decode utilities for TLV ASN.1 DER operations
*/
export function identity(ab) {
return ab;
}
export function bufferToArrayBuffer(buf) {
const out = new ArrayBuffer(buf.byteLength);
new Uint8Array(out).set(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
return out;
}
export function toHex(input) {
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
export function toArrayBuffer(u8) {
const buf = new ArrayBuffer(u8.byteLength);
new Uint8Array(buf).set(u8);
return buf;
}
export function encodeUtf8(str) {
return new TextEncoder().encode(str).buffer;
}
export function decodeUtf8(buffer) {
return new TextDecoder("utf-8").decode(buffer);
}
export function decodeShiftJis(buffer) {
return new TextDecoder("shift_jis").decode(buffer);
}
export function decodeAscii(buffer) {
return new TextDecoder("ascii").decode(buffer);
}
export function encodeInteger(n) {
if (!Number.isFinite(n) || n < 0) {
throw new Error(`INTEGER must be non-negative finite number; got ${n}`);
}
if (n === 0)
return new Uint8Array([0x00]).buffer;
const out = [];
let temp = n;
while (temp > 0) {
out.unshift(temp & 0xff);
temp >>>= 8;
}
if (out[0] & 0x80)
out.unshift(0x00);
return new Uint8Array(out).buffer;
}
export function decodeInteger(buffer) {
const bytes = new Uint8Array(buffer);
if (bytes.length === 0)
return 0;
let i = 0;
if (bytes[0] === 0x00 && bytes.length > 1)
i = 1;
let n = 0;
for (; i < bytes.length; i++)
n = (n << 8) | bytes[i];
return n;
}
function encodeBase128(n) {
if (n === 0)
return [0x00];
const stack = [];
while (n > 0) {
stack.push(n & 0x7f);
n = Math.floor(n / 128);
}
const out = stack.reverse();
for (let i = 0; i < out.length - 1; i++)
out[i] |= 0x80;
return out;
}
export function encodeOID(oid) {
const arcs = oid.split(".").map((s) => {
const n = Number(s);
if (!Number.isFinite(n) || n < 0)
throw new Error(`Invalid OID arc: ${s}`);
return Math.floor(n);
});
if (arcs.length < 2)
throw new Error(`OID must have at least two arcs; got '${oid}'`);
const first = arcs[0];
const second = arcs[1];
let firstByte = 0;
if (first < 2) {
firstByte = first * 40 + second;
}
else {
firstByte = 80 + second;
}
const out = [firstByte];
for (let i = 2; i < arcs.length; i++) {
out.push(...encodeBase128(arcs[i]));
}
return new Uint8Array(out).buffer;
}
export function decodeOID(buffer) {
const bytes = new Uint8Array(buffer);
if (bytes.length === 0)
throw new Error("Empty OID encoding (0 bytes)");
const firstByte = bytes[0];
let first = Math.floor(firstByte / 40);
let second = firstByte % 40;
if (firstByte >= 80) {
first = 2;
second = firstByte - 80;
}
const arcs = [first, second];
let i = 1;
while (i < bytes.length) {
let val = 0;
let b;
do {
if (i >= bytes.length)
throw new Error(`Truncated OID at byte index ${i}`);
b = bytes[i++];
val = (val << 7) | (b & 0x7f);
} while (b & 0x80);
arcs.push(val);
}
return arcs.join(".");
}
export function decodeBitStringHex(buffer) {
const bytes = new Uint8Array(buffer);
if (bytes.length === 0) {
return { unusedBits: 0, hex: "" };
}
const unusedBits = bytes[0];
// DER: unusedBits must be in 0..7 (and fit in a single octet).
if (!Number.isInteger(unusedBits) || unusedBits < 0 || unusedBits > 7) {
throw new Error(`BIT STRING unusedBits must be in range 0..7; got ${unusedBits}`);
}
const content = bytes.slice(1);
// A BIT STRING with non-zero unusedBits must have at least one content byte.
if (content.length === 0 && unusedBits !== 0) {
throw new Error(`BIT STRING with unusedBits=${unusedBits} must have at least one content byte`);
}
if (content.length > 0 && unusedBits !== 0) {
const last = content[content.length - 1];
const mask = (1 << unusedBits) - 1;
if ((last & mask) !== 0) {
throw new Error(`BIT STRING unused bits in last byte must be zero; got 0x${last
.toString(16)
.padStart(2, "0")} with unusedBits=${unusedBits}`);
}
}
return { unusedBits, hex: toHex(content) };
}
export function encodeBitString(bits) {
const buffer = new ArrayBuffer(bits.data.length + 1);
const view = new Uint8Array(buffer);
view[0] = bits.unusedBits;
view.set(bits.data, 1);
return buffer;
}