/** Minimal CBOR decoder covering what WebAuthn attestation objects and COSE keys use. */ export type CborValue = | number | bigint | string | boolean | null | Uint8Array | CborValue[] | Map; const MAX_DEPTH = 16; const MAX_ITEMS = 1024; const INFO_DIRECT_MAX = 24; const INFO_UINT8 = 24; const INFO_UINT16 = 25; const INFO_UINT32 = 26; const INFO_UINT64 = 27; const MAJOR_SIMPLE = 7; const SIMPLE_FALSE = 20; const SIMPLE_TRUE = 21; const SIMPLE_NULL = 22; const SIMPLE_UNDEFINED = 23; export function decodeCbor(bytes: Uint8Array): { value: CborValue; length: number } { const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); let offset = 0; function need(count: number): void { if (offset + count > bytes.byteLength) throw new Error("CBOR: truncated input"); } function readArgument(info: number): number { if (info < INFO_DIRECT_MAX) return info; if (info === INFO_UINT8) { need(1); return view.getUint8(offset++); } if (info === INFO_UINT16) { need(2); const value = view.getUint16(offset); offset += 2; return value; } if (info === INFO_UINT32) { need(4); const value = view.getUint32(offset); offset += 4; return value; } if (info === INFO_UINT64) { need(8); const value = view.getBigUint64(offset); offset += 8; if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("CBOR: integer too large"); return Number(value); } throw new Error("CBOR: indefinite lengths are not supported"); } function readItem(depth: number): CborValue { if (depth > MAX_DEPTH) throw new Error("CBOR: nesting too deep"); need(1); const initial = view.getUint8(offset++); const major = initial >> 5; const info = initial & 0x1f; if (major === MAJOR_SIMPLE) { if (info === SIMPLE_FALSE) return false; if (info === SIMPLE_TRUE) return true; if (info === SIMPLE_NULL || info === SIMPLE_UNDEFINED) return null; throw new Error("CBOR: unsupported simple value"); } const argument = readArgument(info); switch (major) { case 0: return argument; case 1: return -1 - argument; case 2: { need(argument); const value = bytes.slice(offset, offset + argument); offset += argument; return value; } case 3: { need(argument); const value = new TextDecoder().decode(bytes.subarray(offset, offset + argument)); offset += argument; return value; } case 4: { if (argument > MAX_ITEMS) throw new Error("CBOR: array too large"); const items: CborValue[] = []; for (let index = 0; index < argument; index++) items.push(readItem(depth + 1)); return items; } case 5: { if (argument > MAX_ITEMS) throw new Error("CBOR: map too large"); const map = new Map(); for (let index = 0; index < argument; index++) { const key = readItem(depth + 1); map.set(key, readItem(depth + 1)); } return map; } case 6: return readItem(depth + 1); default: throw new Error("CBOR: unsupported major type"); } } const value = readItem(0); return { value, length: offset }; } export function cborMap(value: CborValue): Map { if (!(value instanceof Map)) throw new Error("CBOR: expected a map"); return value; } export function cborBytes(value: CborValue | undefined): Uint8Array { if (!(value instanceof Uint8Array)) throw new Error("CBOR: expected a byte string"); return value; }