12345678910111213141516import {17 CHIRP_MAGIC_BYTES,18 CHIRP_PAYLOAD_LENGTH,19 CHIRP_PROTOCOL_VERSION,20 type ChirpPayload,21 type DeviceTypeByte,22} from "./sync-types";2324const OFFSET_MAGIC = 0;25const OFFSET_VERSION = 4;26const OFFSET_DEVICE_TYPE = 5;27const OFFSET_PUBKEY = 6;28const OFFSET_NONCE = 38;29const OFFSET_CHECKSUM = 54;30const OFFSET_PADDING = 58;31const CHECKSUM_COVERAGE = 54;3233export class ChirpDecodeError extends Error {34 readonly kind:35 | "length"36 | "magic"37 | "version"38 | "device_type"39 | "checksum"40 | "padding";41 constructor(kind: ChirpDecodeError["kind"], message: string) {42 super(message);43 this.kind = kind;44 }45}4647export function encodeChirpPayload(payload: ChirpPayload): Uint8Array {48 if (payload.ephemeralPublicKey.length !== 32) {49 throw new Error("ephemeralPublicKey must be 32 bytes");50 }51 if (payload.sessionNonce.length !== 16) {52 throw new Error("sessionNonce must be 16 bytes");53 }54 const buf = new Uint8Array(CHIRP_PAYLOAD_LENGTH);55 buf.set(CHIRP_MAGIC_BYTES, OFFSET_MAGIC);56 buf[OFFSET_VERSION] = payload.version & 0xff;57 buf[OFFSET_DEVICE_TYPE] = payload.deviceType & 0xff;58 buf.set(payload.ephemeralPublicKey, OFFSET_PUBKEY);59 buf.set(payload.sessionNonce, OFFSET_NONCE);60 const crc = crc32(buf.subarray(0, CHECKSUM_COVERAGE));61 buf[OFFSET_CHECKSUM] = (crc >>> 24) & 0xff;62 buf[OFFSET_CHECKSUM + 1] = (crc >>> 16) & 0xff;63 buf[OFFSET_CHECKSUM + 2] = (crc >>> 8) & 0xff;64 buf[OFFSET_CHECKSUM + 3] = crc & 0xff;65 66 return buf;67}6869export function decodeChirpPayload(buf: Uint8Array): ChirpPayload {70 if (buf.length !== CHIRP_PAYLOAD_LENGTH) {71 throw new ChirpDecodeError(72 "length",73 `chirp payload must be ${CHIRP_PAYLOAD_LENGTH} bytes, got ${buf.length}`,74 );75 }76 for (let i = 0; i < 4; i++) {77 if (buf[OFFSET_MAGIC + i] !== CHIRP_MAGIC_BYTES[i]) {78 throw new ChirpDecodeError("magic", "magic bytes are not PEAC");79 }80 }81 const version = buf[OFFSET_VERSION];82 if (version !== CHIRP_PROTOCOL_VERSION) {83 throw new ChirpDecodeError(84 "version",85 `unsupported chirp protocol version ${version}`,86 );87 }88 const deviceType = buf[OFFSET_DEVICE_TYPE];89 if (deviceType < 0x01 || deviceType > 0x04) {90 throw new ChirpDecodeError(91 "device_type",92 `unknown device type byte 0x${deviceType.toString(16)}`,93 );94 }95 const declared =96 ((buf[OFFSET_CHECKSUM] << 24) |97 (buf[OFFSET_CHECKSUM + 1] << 16) |98 (buf[OFFSET_CHECKSUM + 2] << 8) |99 buf[OFFSET_CHECKSUM + 3]) >>>100 0;101 const computed = crc32(buf.subarray(0, CHECKSUM_COVERAGE));102 if (declared !== computed) {103 throw new ChirpDecodeError("checksum", "CRC32 mismatch");104 }105 for (let i = OFFSET_PADDING; i < CHIRP_PAYLOAD_LENGTH; i++) {106 if (buf[i] !== 0) {107 throw new ChirpDecodeError("padding", "reserved padding bytes must be zero");108 }109 }110 return {111 version,112 deviceType: deviceType as DeviceTypeByte,113 ephemeralPublicKey: buf.slice(OFFSET_PUBKEY, OFFSET_PUBKEY + 32),114 sessionNonce: buf.slice(OFFSET_NONCE, OFFSET_NONCE + 16),115 };116}117118119120let crcTable: Uint32Array | undefined;121122function buildCrcTable(): Uint32Array {123 const table = new Uint32Array(256);124 for (let i = 0; i < 256; i++) {125 let c = i;126 for (let k = 0; k < 8; k++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);127 table[i] = c >>> 0;128 }129 return table;130}131132export function crc32(bytes: Uint8Array): number {133 if (!crcTable) crcTable = buildCrcTable();134 let crc = 0xffffffff;135 for (let i = 0; i < bytes.length; i++) {136 crc = crcTable[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8);137 }138 return (crc ^ 0xffffffff) >>> 0;139}