5 Commits

Author SHA1 Message Date
f5fa45d11c Version 1.2.0 2026-03-11 13:24:09 +01:00
7c2cc0e0f6 Implemented Packet structure decoding 2026-03-11 13:23:52 +01:00
dee5e1cb9e Fixed incorrect path hash parsing 2026-03-10 18:54:53 +01:00
e388a55575 Updated README 2026-03-10 18:44:56 +01:00
c52ec1dc43 Updated package name to include org 2026-03-10 18:39:32 +01:00
7 changed files with 569 additions and 102 deletions

View File

@@ -2,18 +2,101 @@
TypeScript library for MeshCore protocol utilities. TypeScript library for MeshCore protocol utilities.
Quick start ## Packet parsing
1. Install dev dependencies: Using the library to decode MeshCore packets:
```bash ```ts
npm install --save-dev typescript tsup import { Packet } from '@hamradio/meshcore';
const raw = new Uint8Array(Buffer.from("050AA50E2CB0336DB67BBF78928A3BB9BF7A8B677C83B6EC0716F9DD10002A06", "hex"));
const packet = Packet.fromBytes(raw);
console.log(packet);
/*
_Packet {
header: 5,
transport: undefined,
pathLength: 10,
path: Uint8Array(10) [
165, 14, 44, 176,
51, 109, 182, 123,
191, 120
],
payload: Uint8Array(20) [
146, 138, 59, 185, 191, 122,
139, 103, 124, 131, 182, 236,
7, 22, 249, 221, 16, 0,
42, 6
],
routeType: 1,
payloadVersion: 0,
payloadType: 1,
pathHashCount: 10,
pathHashSize: 1,
pathHashBytes: 10,
pathHashes: [
'a5', '0e', '2c',
'b0', '33', '6d',
'b6', '7b', 'bf',
'78'
]
}
*/
``` ```
2. Build the library: ## Packet structure parsing
```bash The parser can also be instructed to generate a packet structure, useful for debugging or
npm run build printing packet details:
```ts
import { Packet } from '@hamradio/meshcore';
const raw = new Uint8Array(Buffer.from("050AA50E2CB0336DB67BBF78928A3BB9BF7A8B677C83B6EC0716F9DD10002A06", "hex"));
const packet = Packet.fromBytes(raw);
const { structure } = packet.decode(true);
console.log(structure);
/*
[
{
name: 'header',
data: Uint8Array(12) [
5, 10, 165, 14, 44,
176, 51, 109, 182, 123,
191, 120
],
fields: [
{ name: 'flags', type: 0, size: 1, bits: [Array] },
{ name: 'path length', type: 1, size: 1, bits: [Array] },
{ name: 'path hashes', type: 6, size: 10 }
]
},
{
name: 'response payload',
data: Uint8Array(20) [
146, 138, 59, 185, 191, 122,
139, 103, 124, 131, 182, 236,
7, 22, 249, 221, 16, 0,
42, 6
],
fields: [
{ name: 'destination hash', type: 1, size: 1, value: 146 },
{ name: 'source hash', type: 1, size: 1, value: 138 },
{ name: 'cipher MAC', type: 6, size: 2, value: [Uint8Array] },
{ name: 'cipher text', type: 6, size: 16, value: [Uint8Array] }
]
}
]
*/
``` ```
3. Use the build output from the `dist/` folder or publish to npm. ## Identities
The package supports:
- `Identity` for public key management.
- `LocalIdentity` for private key management.
- `Contact` for managing named identities.
- `Group` for managing groups.
- `KeyManager` for managing all of the above and handling decryption.

View File

@@ -1,6 +1,6 @@
{ {
"name": "meshcore", "name": "@hamradio/meshcore",
"version": "1.1.1", "version": "1.2.0",
"description": "MeshCore protocol support for Typescript", "description": "MeshCore protocol support for Typescript",
"keywords": [ "keywords": [
"MeshCore", "MeshCore",
@@ -9,7 +9,7 @@
], ],
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://git.maze.io/ham/meshcore.js" "url": "https://git.maze.io/ham/meshcore.js"
}, },
"license": "MIT", "license": "MIT",
"author": "Wijnand Modderman-Lenstra", "author": "Wijnand Modderman-Lenstra",

View File

@@ -1,11 +1,50 @@
export * from './identity'; export {
export * from './identity.types'; type IPacket,
export type * from './identity.types'; type Payload,
type EncryptedPayload,
type RequestPayload,
type ResponsePayload,
type TextPayload,
type AckPayload,
type AdvertPayload,
type GroupTextPayload,
type GroupDataPayload,
type AnonReqPayload,
type PathPayload,
type TracePayload,
type RawCustomPayload,
RouteType,
PayloadType,
RequestType,
TextType,
NodeType,
} from "./packet.types";
export { Packet } from "./packet";
export * from './crypto'; export {
export * from './crypto.types'; type NodeHash,
export type * from './crypto.types'; type IIdentity,
type ILocalIdentity,
type IContact
} from "./identity.types";
export {
parseNodeHash,
Identity,
LocalIdentity,
Contact,
Group,
Contacts
} from "./identity";
export * from './packet'; export {
export * from './packet.types'; type IPublicKey,
export type * from './packet.types'; type IPrivateKey,
type ISharedSecret,
type IStaticSecret
} from "./crypto.types";
export {
PublicKey,
PrivateKey,
SharedSecret,
StaticSecret,
} from "./crypto";

View File

@@ -2,6 +2,7 @@ import { sha256 } from "@noble/hashes/sha2.js";
import { import {
AckPayload, AckPayload,
AdvertAppData, AdvertAppData,
AdvertFlag,
AdvertPayload, AdvertPayload,
AnonReqPayload, AnonReqPayload,
EncryptedPayload, EncryptedPayload,
@@ -24,6 +25,7 @@ import {
BufferReader, BufferReader,
bytesToHex bytesToHex
} from "./parser"; } from "./parser";
import { FieldType, PacketSegment, PacketStructure } from "./parser.types";
export class Packet implements IPacket { export class Packet implements IPacket {
// Raw packet bytes. // Raw packet bytes.
@@ -40,6 +42,8 @@ export class Packet implements IPacket {
public pathHashSize: number; public pathHashSize: number;
public pathHashBytes: number; public pathHashBytes: number;
public pathHashes: string[]; public pathHashes: string[];
// Parsed packet segments.
public structure?: PacketStructure | undefined;
constructor(header: number, transport: [number, number] | undefined, pathLength: number, path: Uint8Array, payload: Uint8Array) { constructor(header: number, transport: [number, number] | undefined, pathLength: number, path: Uint8Array, payload: Uint8Array) {
this.header = header; this.header = header;
@@ -52,13 +56,13 @@ export class Packet implements IPacket {
this.payloadVersion = (header >> 6) & 0x03; this.payloadVersion = (header >> 6) & 0x03;
this.payloadType = (header >> 2) & 0x0f; this.payloadType = (header >> 2) & 0x0f;
this.pathHashCount = (pathLength >> 6) + 1; this.pathHashSize = (pathLength >> 6) + 1;
this.pathHashSize = pathLength & 0x3f; this.pathHashCount = pathLength & 0x3f;
this.pathHashBytes = this.pathHashCount * this.pathHashSize; this.pathHashBytes = this.pathHashCount * this.pathHashSize;
this.pathHashes = []; this.pathHashes = [];
for (let i = 0; i < this.pathHashCount; i++) { for (let i = 0; i < this.pathHashBytes; i += this.pathHashSize) {
const hashBytes = this.path.slice(i * this.pathHashSize, (i + 1) * this.pathHashSize); const hashBytes = this.path.slice(i, i + this.pathHashSize);
const hashHex = bytesToHex(hashBytes); const hashHex = bytesToHex(hashBytes);
this.pathHashes.push(hashHex); this.pathHashes.push(hashHex);
} }
@@ -99,33 +103,117 @@ export class Packet implements IPacket {
return bytesToHex(digest.slice(0, 8)); return bytesToHex(digest.slice(0, 8));
} }
public decode(): Payload { private ensureStructure(): void {
if (typeof this.structure !== "undefined") {
return;
}
let pathHashType: FieldType
switch (this.pathHashSize) {
case 1: pathHashType = FieldType.BYTES; break;
case 2: pathHashType = FieldType.WORDS; break;
case 4: pathHashType = FieldType.DWORDS; break;
default:
throw new Error(`Unsupported path hash size: ${this.pathHashSize}`);
}
this.structure = [
/* Header segment */
{ name: "header", data: new Uint8Array([this.header, this.pathLength, ...this.path]), fields: [
/* Header flags */
{
name: "flags",
type: FieldType.BITS,
size: 1,
bits: [
{ name: "route type", size: 2 },
{ name: "payload version", size: 2 },
{ name: "payload type", size: 4 },
]
},
/* Transport codes */
...(Packet.hasTransportCodes(this.routeType) ? [
{
name: "transport code 1",
type: FieldType.UINT16_BE,
size: 2
},
{
name: "transport code 2",
type: FieldType.UINT16_BE,
size: 2
},
] : []),
/* Path length and hashes */
{
name: "path length",
type: FieldType.UINT8,
size: 1,
bits: [
{ name: "path hash size", size: 2 },
{ name: "path hash count", size: 6 },
]
},
{
name: "path hashes",
type: pathHashType,
size: this.path.length
}
]},
]
}
public decode(withStructure?: boolean): Payload | { payload: Payload, structure: PacketStructure } {
let result: Payload | { payload: Payload, segment: PacketSegment };
switch (this.payloadType) { switch (this.payloadType) {
case PayloadType.REQUEST: case PayloadType.REQUEST:
return this.decodeRequest(); result = this.decodeRequest(withStructure);
break;
case PayloadType.RESPONSE: case PayloadType.RESPONSE:
return this.decodeResponse(); result = this.decodeResponse(withStructure);
break;
case PayloadType.TEXT: case PayloadType.TEXT:
return this.decodeText(); result = this.decodeText(withStructure);
break;
case PayloadType.ACK: case PayloadType.ACK:
return this.decodeAck(); result = this.decodeAck(withStructure);
break;
case PayloadType.ADVERT: case PayloadType.ADVERT:
return this.decodeAdvert(); result = this.decodeAdvert(withStructure);
break;
case PayloadType.GROUP_TEXT: case PayloadType.GROUP_TEXT:
return this.decodeGroupText(); result = this.decodeGroupText(withStructure);
break;
case PayloadType.GROUP_DATA: case PayloadType.GROUP_DATA:
return this.decodeGroupData(); result = this.decodeGroupData(withStructure);
break;
case PayloadType.ANON_REQ: case PayloadType.ANON_REQ:
return this.decodeAnonReq(); result = this.decodeAnonReq(withStructure);
break;
case PayloadType.PATH: case PayloadType.PATH:
return this.decodePath(); result = this.decodePath(withStructure);
break;
case PayloadType.TRACE: case PayloadType.TRACE:
return this.decodeTrace(); result = this.decodeTrace(withStructure);
break;
case PayloadType.RAW_CUSTOM: case PayloadType.RAW_CUSTOM:
return this.decodeRawCustom(); result = this.decodeRawCustom(withStructure);
break;
default: default:
throw new Error(`Unsupported payload type: ${this.payloadType}`); throw new Error(`Unsupported payload type: ${this.payloadType}`);
} }
console.log('packet decode with structure:', typeof withStructure, withStructure, { result });
if (typeof withStructure === "boolean" && withStructure && "segment" in result && "payload" in result) {
this.ensureStructure();
const structure = [ ...this.structure!, result.segment ];
return { payload: result.payload, structure };
}
return result as Payload;
} }
private decodeEncryptedPayload(reader: BufferReader): EncryptedPayload { private decodeEncryptedPayload(reader: BufferReader): EncryptedPayload {
@@ -134,61 +222,128 @@ export class Packet implements IPacket {
return { cipherMAC, cipherText }; return { cipherMAC, cipherText };
} }
private decodeRequest(): RequestPayload { private decodeRequest(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } {
if (this.payload.length < 4) { if (this.payload.length < 4) {
throw new Error("Invalid request payload: too short"); throw new Error("Invalid request payload: too short");
} }
const reader = new BufferReader(this.payload); const reader = new BufferReader(this.payload);
return { const dst = reader.readByte();
const src = reader.readByte();
const encrypted = this.decodeEncryptedPayload(reader);
const payload: RequestPayload = {
type: PayloadType.REQUEST, type: PayloadType.REQUEST,
dst: reader.readByte(), dst,
src: reader.readByte(), src,
encrypted: this.decodeEncryptedPayload(reader), encrypted,
};
if (typeof withSegment === "boolean" && withSegment) {
const segment = {
name: "request payload",
data: this.payload,
fields: [
{ name: "destination hash", type: FieldType.UINT8, size: 1, value: dst },
{ name: "source hash", type: FieldType.UINT8, size: 1, value: src },
{ name: "cipher MAC", type: FieldType.BYTES, size: 2, value: encrypted.cipherMAC },
{ name: "cipher text", type: FieldType.BYTES, size: encrypted.cipherText.length, value: encrypted.cipherText }
]
} }
return { payload, segment };
}
return payload;
} }
private decodeResponse(): ResponsePayload { private decodeResponse(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } {
if (this.payload.length < 4) { if (this.payload.length < 4) {
throw new Error("Invalid response payload: too short"); throw new Error("Invalid response payload: too short");
} }
const reader = new BufferReader(this.payload); const reader = new BufferReader(this.payload);
return { const dst = reader.readByte();
const src = reader.readByte();
const encrypted = this.decodeEncryptedPayload(reader);
const payload: ResponsePayload = {
type: PayloadType.RESPONSE, type: PayloadType.RESPONSE,
dst: reader.readByte(), dst,
src: reader.readByte(), src,
encrypted: this.decodeEncryptedPayload(reader), encrypted,
};
if (typeof withSegment === "boolean" && withSegment) {
const segment = {
name: "response payload",
data: this.payload,
fields: [
{ name: "destination hash", type: FieldType.UINT8, size: 1, value: dst },
{ name: "source hash", type: FieldType.UINT8, size: 1, value: src },
{ name: "cipher MAC", type: FieldType.BYTES, size: 2, value: encrypted.cipherMAC },
{ name: "cipher text", type: FieldType.BYTES, size: encrypted.cipherText.length, value: encrypted.cipherText }
]
};
return { payload, segment };
} }
return payload;
} }
private decodeText(): TextPayload { private decodeText(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } {
if (this.payload.length < 4) { if (this.payload.length < 4) {
throw new Error("Invalid text payload: too short"); throw new Error("Invalid text payload: too short");
} }
const reader = new BufferReader(this.payload); const reader = new BufferReader(this.payload);
return { const dst = reader.readByte();
const src = reader.readByte();
const encrypted = this.decodeEncryptedPayload(reader);
const payload: TextPayload = {
type: PayloadType.TEXT, type: PayloadType.TEXT,
dst: reader.readByte(), dst,
src: reader.readByte(), src,
encrypted: this.decodeEncryptedPayload(reader), encrypted,
};
if (typeof withSegment === "boolean" && withSegment) {
const segment = {
name: "text payload",
data: this.payload,
fields: [
{ name: "destination hash", type: FieldType.UINT8, size: 1, value: dst },
{ name: "source hash", type: FieldType.UINT8, size: 1, value: src },
{ name: "cipher MAC", type: FieldType.BYTES, size: 2, value: encrypted.cipherMAC },
{ name: "cipher text", type: FieldType.BYTES, size: encrypted.cipherText.length, value: encrypted.cipherText }
]
};
return { payload, segment };
} }
return payload;
} }
private decodeAck(): AckPayload { private decodeAck(withSegment?: boolean): Payload | { payload: AckPayload, segment: PacketSegment } {
if (this.payload.length < 4) { if (this.payload.length < 4) {
throw new Error("Invalid ack payload: too short"); throw new Error("Invalid ack payload: too short");
} }
const reader = new BufferReader(this.payload); const reader = new BufferReader(this.payload);
return { const checksum = reader.readBytes(4);
const payload: AckPayload = {
type: PayloadType.ACK, type: PayloadType.ACK,
checksum: reader.readBytes(4), checksum,
};
if (typeof withSegment === "boolean" && withSegment) {
const segment = {
name: "ack payload",
data: this.payload,
fields: [
{ name: "checksum", type: FieldType.BYTES, size: 4, value: checksum }
]
};
return { payload, segment };
} }
return payload;
} }
private decodeAdvert(): AdvertPayload { private decodeAdvert(withSegment?: boolean): Payload | { payload: AdvertPayload, segment: PacketSegment } {
if (this.payload.length < 4) { if (this.payload.length < 4) {
throw new Error("Invalid advert payload: too short"); throw new Error("Invalid advert payload: too short");
} }
@@ -201,25 +356,57 @@ export class Packet implements IPacket {
signature: reader.readBytes(64), signature: reader.readBytes(64),
} }
let segment: PacketSegment | undefined;
if (typeof withSegment === "boolean" && withSegment) {
segment = {
name: "advert payload",
data: this.payload,
fields: [
{ type: FieldType.BYTES, name: "public key", size: 32 },
{ type: FieldType.UINT32_LE, name: "timestamp", size: 4, value: payload.timestamp! },
{ type: FieldType.BYTES, name: "signature", size: 64 },
]
};
}
const flags = reader.readByte(); const flags = reader.readByte();
const appdata: AdvertAppData = { const appdata: AdvertAppData = {
nodeType: flags & 0x0f, nodeType: flags & 0x0f,
hasLocation: (flags & 0x10) !== 0, hasLocation: (flags & AdvertFlag.HAS_LOCATION) !== 0,
hasFeature1: (flags & 0x20) !== 0, hasFeature1: (flags & AdvertFlag.HAS_FEATURE1) !== 0,
hasFeature2: (flags & 0x40) !== 0, hasFeature2: (flags & AdvertFlag.HAS_FEATURE2) !== 0,
hasName: (flags & 0x80) !== 0, hasName: (flags & AdvertFlag.HAS_NAME) !== 0,
}
if (typeof withSegment === "boolean" && withSegment) {
segment!.fields.push({ type: FieldType.BITS, name: "flags", size: 1, value: flags, bits: [
{ size: 4, name: "node type" },
{ size: 1, name: "location flag" },
{ size: 1, name: "feature1 flag" },
{ size: 1, name: "feature2 flag" },
{ size: 1, name: "name flag" },
]});
} }
if (appdata.hasLocation) { if (appdata.hasLocation) {
const lat = reader.readInt32LE() / 100000; const lat = reader.readInt32LE() / 100000;
const lon = reader.readInt32LE() / 100000; const lon = reader.readInt32LE() / 100000;
appdata.location = [lat, lon]; appdata.location = [lat, lon];
if (typeof withSegment === "boolean" && withSegment) {
segment!.fields.push({ type: FieldType.UINT32_LE, name: "latitude", size: 4, value: lat });
segment!.fields.push({ type: FieldType.UINT32_LE, name: "longitude", size: 4, value: lon });
}
} }
if (appdata.hasFeature1) { if (appdata.hasFeature1) {
appdata.feature1 = reader.readUint16LE(); appdata.feature1 = reader.readUint16LE();
if (typeof withSegment === "boolean" && withSegment) {
segment!.fields.push({ type: FieldType.UINT16_LE, name: "feature1", size: 2, value: appdata.feature1 });
}
} }
if (appdata.hasFeature2) { if (appdata.hasFeature2) {
appdata.feature2 = reader.readUint16LE(); appdata.feature2 = reader.readUint16LE();
if (typeof withSegment === "boolean" && withSegment) {
segment!.fields.push({ type: FieldType.UINT16_LE, name: "feature2", size: 2, value: appdata.feature2 });
}
} }
if (appdata.hasName) { if (appdata.hasName) {
const nameBytes = reader.readBytes(); const nameBytes = reader.readBytes();
@@ -228,86 +415,175 @@ export class Packet implements IPacket {
nullPos = nameBytes.length; nullPos = nameBytes.length;
} }
appdata.name = new TextDecoder('utf-8').decode(nameBytes.subarray(0, nullPos)); appdata.name = new TextDecoder('utf-8').decode(nameBytes.subarray(0, nullPos));
if (typeof withSegment === "boolean" && withSegment) {
segment!.fields.push({ type: FieldType.C_STRING, name: "name", size: nameBytes.length, value: appdata.name });
}
} }
return { if (typeof withSegment === "boolean" && withSegment && typeof segment !== "undefined") {
...payload, return { payload: { ...payload, appdata } as AdvertPayload, segment };
appdata
} as AdvertPayload;
} }
private decodeGroupText(): GroupTextPayload { return { ...payload, appdata } as AdvertPayload;
}
private decodeGroupText(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } {
if (this.payload.length < 3) { if (this.payload.length < 3) {
throw new Error("Invalid group text payload: too short"); throw new Error("Invalid group text payload: too short");
} }
const reader = new BufferReader(this.payload); const reader = new BufferReader(this.payload);
return { const channelHash = reader.readByte();
const encrypted = this.decodeEncryptedPayload(reader);
const payload: GroupTextPayload = {
type: PayloadType.GROUP_TEXT, type: PayloadType.GROUP_TEXT,
channelHash: reader.readByte(), channelHash,
encrypted: this.decodeEncryptedPayload(reader), encrypted,
};
if (typeof withSegment === "boolean" && withSegment) {
const segment = {
name: "group text payload",
data: this.payload,
fields: [
{ name: "channel hash", type: FieldType.UINT8, size: 1, value: channelHash },
{ name: "cipher MAC", type: FieldType.BYTES, size: 2, value: encrypted.cipherMAC },
{ name: "cipher text", type: FieldType.BYTES, size: encrypted.cipherText.length, value: encrypted.cipherText }
]
};
return { payload, segment };
} }
return payload;
} }
private decodeGroupData(): GroupDataPayload { private decodeGroupData(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } {
if (this.payload.length < 3) { if (this.payload.length < 3) {
throw new Error("Invalid group data payload: too short"); throw new Error("Invalid group data payload: too short");
} }
const reader = new BufferReader(this.payload); const reader = new BufferReader(this.payload);
return { const payload: GroupDataPayload = {
type: PayloadType.GROUP_DATA, type: PayloadType.GROUP_DATA,
channelHash: reader.readByte(), channelHash: reader.readByte(),
encrypted: this.decodeEncryptedPayload(reader), encrypted: this.decodeEncryptedPayload(reader),
};
if (typeof withSegment === "boolean" && withSegment) {
const segment = {
name: "group data payload",
data: this.payload,
fields: [
{ name: "channel hash", type: FieldType.UINT8, size: 1, value: payload.channelHash },
{ name: "cipher MAC", type: FieldType.BYTES, size: 2, value: payload.encrypted.cipherMAC },
{ name: "cipher text", type: FieldType.BYTES, size: payload.encrypted.cipherText.length, value: payload.encrypted.cipherText }
]
};
return { payload, segment };
} }
return payload;
} }
private decodeAnonReq(): AnonReqPayload { private decodeAnonReq(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } {
if (this.payload.length < 1 + 32 + 2) { if (this.payload.length < 1 + 32 + 2) {
throw new Error("Invalid anon req payload: too short"); throw new Error("Invalid anon req payload: too short");
} }
const reader = new BufferReader(this.payload); const reader = new BufferReader(this.payload);
return { const payload: AnonReqPayload = {
type: PayloadType.ANON_REQ, type: PayloadType.ANON_REQ,
dst: reader.readByte(), dst: reader.readByte(),
publicKey: reader.readBytes(32), publicKey: reader.readBytes(32),
encrypted: this.decodeEncryptedPayload(reader), encrypted: this.decodeEncryptedPayload(reader),
} }
if (typeof withSegment === "boolean" && withSegment) {
const segment = {
name: "anon req payload",
data: this.payload,
fields: [
{ name: "destination hash", type: FieldType.UINT8, size: 1, value: payload.dst },
{ name: "public key", type: FieldType.BYTES, size: 32, value: payload.publicKey },
{ name: "cipher MAC", type: FieldType.BYTES, size: 2, value: payload.encrypted.cipherMAC },
{ name: "cipher text", type: FieldType.BYTES, size: payload.encrypted.cipherText.length, value: payload.encrypted.cipherText }
]
};
return { payload, segment };
}
return payload;
} }
private decodePath(): PathPayload { private decodePath(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } {
if (this.payload.length < 2) { if (this.payload.length < 2) {
throw new Error("Invalid path payload: too short"); throw new Error("Invalid path payload: too short");
} }
const reader = new BufferReader(this.payload); const reader = new BufferReader(this.payload);
return { const payload: PathPayload = {
type: PayloadType.PATH, type: PayloadType.PATH,
dst: reader.readByte(), dst: reader.readByte(),
src: reader.readByte(), src: reader.readByte(),
};
if (typeof withSegment === "boolean" && withSegment) {
const segment = {
name: "path payload",
data: this.payload,
fields: [
{ name: "destination hash", type: FieldType.UINT8, size: 1, value: payload.dst },
{ name: "source hash", type: FieldType.UINT8, size: 1, value: payload.src }
]
};
return { payload, segment };
} }
return payload;
} }
private decodeTrace(): TracePayload { private decodeTrace(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } {
if (this.payload.length < 9) { if (this.payload.length < 9) {
throw new Error("Invalid trace payload: too short"); throw new Error("Invalid trace payload: too short");
} }
const reader = new BufferReader(this.payload); const reader = new BufferReader(this.payload);
return { const payload: TracePayload = {
type: PayloadType.TRACE, type: PayloadType.TRACE,
tag: reader.readUint32LE() >>> 0, tag: reader.readUint32LE() >>> 0,
authCode: reader.readUint32LE() >>> 0, authCode: reader.readUint32LE() >>> 0,
flags: reader.readByte() & 0x03, flags: reader.readByte() & 0x03,
nodes: reader.readBytes() nodes: reader.readBytes()
};
if (typeof withSegment === "boolean" && withSegment) {
const segment = {
name: "trace payload",
data: this.payload,
fields: [
{ name: "tag", type: FieldType.DWORDS, size: 4, value: payload.tag },
{ name: "auth code", type: FieldType.DWORDS, size: 4, value: payload.authCode },
{ name: "flags", type: FieldType.UINT8, size: 1, value: payload.flags },
{ name: "nodes", type: FieldType.BYTES, size: payload.nodes.length, value: payload.nodes }
]
};
return { payload, segment };
} }
return payload;
} }
private decodeRawCustom(): RawCustomPayload { private decodeRawCustom(withSegment?: boolean): Payload | { payload: Payload, segment: PacketSegment } {
return { const payload: RawCustomPayload = {
type: PayloadType.RAW_CUSTOM, type: PayloadType.RAW_CUSTOM,
data: this.payload, data: this.payload,
} };
if (typeof withSegment === "boolean" && withSegment) {
const segment = {
name: "raw custom payload",
data: this.payload,
fields: [
{ name: "data", type: FieldType.BYTES, size: this.payload.length, value: this.payload }
]
};
return { payload, segment };
}
return payload;
} }
} }

View File

@@ -1,4 +1,5 @@
import { NodeHash } from "./identity.types"; import { NodeHash } from "./identity.types";
import { PacketStructure } from "./parser.types";
// IPacket contains the raw packet bytes. // IPacket contains the raw packet bytes.
export type Uint16 = number; // 0..65535 export type Uint16 = number; // 0..65535
@@ -12,7 +13,7 @@ export interface IPacket {
path: Uint8Array; path: Uint8Array;
payload: Uint8Array; payload: Uint8Array;
decode(): Payload; decode(withStructure?: boolean): Payload | { payload: Payload, structure: PacketStructure }
} }
export enum RouteType { export enum RouteType {
@@ -36,7 +37,7 @@ export enum PayloadType {
RAW_CUSTOM = 0x0f, RAW_CUSTOM = 0x0f,
} }
export type Payload = export type Payload = BasePayload & (
| RequestPayload | RequestPayload
| ResponsePayload | ResponsePayload
| TextPayload | TextPayload
@@ -47,14 +48,19 @@ export type Payload =
| AnonReqPayload | AnonReqPayload
| PathPayload | PathPayload
| TracePayload | TracePayload
| RawCustomPayload; | RawCustomPayload
);
export interface BasePayload {
type: PayloadType;
}
export interface EncryptedPayload { export interface EncryptedPayload {
cipherMAC: Uint8Array; cipherMAC: Uint8Array;
cipherText: Uint8Array; cipherText: Uint8Array;
} }
export interface RequestPayload { export interface RequestPayload extends BasePayload {
type: PayloadType.REQUEST; type: PayloadType.REQUEST;
dst: NodeHash; dst: NodeHash;
src: NodeHash; src: NodeHash;
@@ -78,7 +84,7 @@ export interface DecryptedRequest {
requestData: Uint8Array; requestData: Uint8Array;
} }
export interface ResponsePayload { export interface ResponsePayload extends BasePayload {
type: PayloadType.RESPONSE; type: PayloadType.RESPONSE;
dst: NodeHash; dst: NodeHash;
src: NodeHash; src: NodeHash;
@@ -91,7 +97,7 @@ export interface DecryptedResponse {
responseData: Uint8Array; responseData: Uint8Array;
} }
export interface TextPayload { export interface TextPayload extends BasePayload {
type: PayloadType.TEXT; type: PayloadType.TEXT;
dst: NodeHash; dst: NodeHash;
src: NodeHash; src: NodeHash;
@@ -112,12 +118,12 @@ export interface DecryptedText {
message: string; message: string;
} }
export interface AckPayload { export interface AckPayload extends BasePayload {
type: PayloadType.ACK; type: PayloadType.ACK;
checksum: Uint8Array; checksum: Uint8Array;
} }
export interface AdvertPayload { export interface AdvertPayload extends BasePayload {
type: PayloadType.ADVERT; type: PayloadType.ADVERT;
publicKey: Uint8Array; publicKey: Uint8Array;
timestamp: Date; timestamp: Date;
@@ -132,6 +138,13 @@ export enum NodeType {
SENSOR_NODE = 0x04, SENSOR_NODE = 0x04,
} }
export enum AdvertFlag {
HAS_LOCATION = 0x10,
HAS_FEATURE1 = 0x20,
HAS_FEATURE2 = 0x40,
HAS_NAME = 0x80,
}
export interface AdvertAppData { export interface AdvertAppData {
nodeType: NodeType; nodeType: NodeType;
hasLocation: boolean; hasLocation: boolean;
@@ -144,7 +157,7 @@ export interface AdvertAppData {
name?: string; name?: string;
} }
export interface GroupTextPayload { export interface GroupTextPayload extends BasePayload {
type: PayloadType.GROUP_TEXT; type: PayloadType.GROUP_TEXT;
channelHash: NodeHash; channelHash: NodeHash;
encrypted: EncryptedPayload; encrypted: EncryptedPayload;
@@ -158,7 +171,7 @@ export interface DecryptedGroupText {
message: string; message: string;
} }
export interface GroupDataPayload { export interface GroupDataPayload extends BasePayload {
type: PayloadType.GROUP_DATA; type: PayloadType.GROUP_DATA;
channelHash: NodeHash; channelHash: NodeHash;
encrypted: EncryptedPayload; encrypted: EncryptedPayload;
@@ -170,7 +183,7 @@ export interface DecryptedGroupData {
data: Uint8Array; data: Uint8Array;
} }
export interface AnonReqPayload { export interface AnonReqPayload extends BasePayload {
type: PayloadType.ANON_REQ; type: PayloadType.ANON_REQ;
dst: NodeHash; dst: NodeHash;
publicKey: Uint8Array; publicKey: Uint8Array;
@@ -183,13 +196,13 @@ export interface DecryptedAnonReq {
data: Uint8Array; data: Uint8Array;
} }
export interface PathPayload { export interface PathPayload extends BasePayload {
type: PayloadType.PATH; type: PayloadType.PATH;
dst: NodeHash; dst: NodeHash;
src: NodeHash; src: NodeHash;
} }
export interface TracePayload { export interface TracePayload extends BasePayload {
type: PayloadType.TRACE; type: PayloadType.TRACE;
tag: number; tag: number;
authCode: number; authCode: number;
@@ -197,7 +210,7 @@ export interface TracePayload {
nodes: Uint8Array; nodes: Uint8Array;
} }
export interface RawCustomPayload { export interface RawCustomPayload extends BasePayload {
type: PayloadType.RAW_CUSTOM; type: PayloadType.RAW_CUSTOM;
data: Uint8Array; data: Uint8Array;
} }

35
src/parser.types.ts Normal file
View File

@@ -0,0 +1,35 @@
export enum FieldType {
BITS = 0,
UINT8 = 1,
UINT16_LE = 2,
UINT16_BE = 3,
UINT32_LE = 4,
UINT32_BE = 5,
BYTES = 6, // 8-bits per value
WORDS = 7, // 16-bits per value
DWORDS = 8, // 32-bits per value
QWORDS = 9, // 64-bits per value
C_STRING = 10,
}
// Interface for the parsed packet segments, used for debugging and testing.
export type PacketStructure = PacketSegment[];
export interface PacketSegment {
name: string;
data: Uint8Array;
fields: PacketField[];
}
export interface PacketField {
type: FieldType;
size: number; // Size in bytes
name?: string;
bits?: PacketFieldBit[]; // Only for bit fields in FieldType.BITS
value?: any; // Optional decoded value
}
export interface PacketFieldBit {
name: string;
size: number; // Size in bits
}

View File

@@ -1,6 +1,6 @@
import { describe, expect, test } from 'vitest'; import { describe, expect, test } from 'vitest';
import { Packet } from '../src/packet'; import { Packet } from '../src/packet';
import { PayloadType, RouteType, NodeType, TracePayload, AdvertPayload, RequestPayload, TextPayload, ResponsePayload, RawCustomPayload, AnonReqPayload } from '../src/packet.types'; import { PayloadType, RouteType, NodeType, TracePayload, AdvertPayload, RequestPayload, TextPayload, ResponsePayload, RawCustomPayload, AnonReqPayload, Payload, AckPayload, PathPayload, GroupDataPayload, GroupTextPayload } from '../src/packet.types';
import { hexToBytes, bytesToHex } from '../src/parser'; import { hexToBytes, bytesToHex } from '../src/parser';
describe('Packet.fromBytes', () => { describe('Packet.fromBytes', () => {
@@ -51,7 +51,7 @@ describe('Packet.fromBytes', () => {
const pkt = Packet.fromBytes(bytes); const pkt = Packet.fromBytes(bytes);
expect(pkt.routeType).toBe(RouteType.TRANSPORT_DIRECT); expect(pkt.routeType).toBe(RouteType.TRANSPORT_DIRECT);
expect(pkt.payloadType).toBe(PayloadType.TRACE); expect(pkt.payloadType).toBe(PayloadType.TRACE);
const payload = pkt.decode(); const payload = pkt.decode() as TracePayload;
expect(payload.type).toBe(PayloadType.TRACE); expect(payload.type).toBe(PayloadType.TRACE);
// the TRACE payload format has been updated; ensure we decode a TRACE payload // the TRACE payload format has been updated; ensure we decode a TRACE payload
expect(payload.type).toBe(PayloadType.TRACE); expect(payload.type).toBe(PayloadType.TRACE);
@@ -148,7 +148,7 @@ describe('Packet decode branches and transport/path parsing', () => {
test('ACK decode and RAW_CUSTOM', () => { test('ACK decode and RAW_CUSTOM', () => {
const ackPayload = new Uint8Array([0x01,0x02,0x03,0x04]); const ackPayload = new Uint8Array([0x01,0x02,0x03,0x04]);
const ack = Packet.fromBytes(makePacket(PayloadType.ACK, RouteType.DIRECT, new Uint8Array([]), ackPayload)).decode(); const ack = Packet.fromBytes(makePacket(PayloadType.ACK, RouteType.DIRECT, new Uint8Array([]), ackPayload)).decode() as AckPayload;
expect(ack.type).toBe(PayloadType.ACK); expect(ack.type).toBe(PayloadType.ACK);
const custom = new Uint8Array([0x99,0x88,0x77]); const custom = new Uint8Array([0x99,0x88,0x77]);
@@ -173,9 +173,9 @@ describe('Packet decode branches and transport/path parsing', () => {
test('GROUP_TEXT and GROUP_DATA decode', () => { test('GROUP_TEXT and GROUP_DATA decode', () => {
const payload = new Uint8Array([0x55, 0x01, 0x02, 0x03]); // channelHash + mac(2) + cipher const payload = new Uint8Array([0x55, 0x01, 0x02, 0x03]); // channelHash + mac(2) + cipher
const gt = Packet.fromBytes(makePacket(PayloadType.GROUP_TEXT, RouteType.DIRECT, new Uint8Array([]), payload)).decode(); const gt = Packet.fromBytes(makePacket(PayloadType.GROUP_TEXT, RouteType.DIRECT, new Uint8Array([]), payload)).decode() as GroupTextPayload;
expect(gt.type).toBe(PayloadType.GROUP_TEXT); expect(gt.type).toBe(PayloadType.GROUP_TEXT);
const gd = Packet.fromBytes(makePacket(PayloadType.GROUP_DATA, RouteType.DIRECT, new Uint8Array([]), payload)).decode(); const gd = Packet.fromBytes(makePacket(PayloadType.GROUP_DATA, RouteType.DIRECT, new Uint8Array([]), payload)).decode() as GroupDataPayload;
expect(gd.type).toBe(PayloadType.GROUP_DATA); expect(gd.type).toBe(PayloadType.GROUP_DATA);
}); });
@@ -191,7 +191,7 @@ describe('Packet decode branches and transport/path parsing', () => {
test('PATH and TRACE decode nodes', () => { test('PATH and TRACE decode nodes', () => {
const pathPayload = new Uint8Array([0x0a, 0x0b]); const pathPayload = new Uint8Array([0x0a, 0x0b]);
const path = Packet.fromBytes(makePacket(PayloadType.PATH, RouteType.DIRECT, new Uint8Array([]), pathPayload)).decode(); const path = Packet.fromBytes(makePacket(PayloadType.PATH, RouteType.DIRECT, new Uint8Array([]), pathPayload)).decode() as PathPayload;
expect(path.type).toBe(PayloadType.PATH); expect(path.type).toBe(PayloadType.PATH);
const nodes = new Uint8Array([0x01,0x02,0x03]); const nodes = new Uint8Array([0x01,0x02,0x03]);
@@ -217,10 +217,10 @@ describe('Packet decode branches and transport/path parsing', () => {
arr.set(pathBytes, parts.length); arr.set(pathBytes, parts.length);
arr.set(payload, parts.length + pathBytes.length); arr.set(payload, parts.length + pathBytes.length);
const pkt = Packet.fromBytes(arr); const pkt = Packet.fromBytes(arr);
expect(pkt.pathHashCount).toBe(2); expect(pkt.pathHashCount).toBe(3);
expect(pkt.pathHashSize).toBe(3); expect(pkt.pathHashSize).toBe(2);
expect(pkt.pathHashes.length).toBe(2); expect(pkt.pathHashes.length).toBe(3);
expect(pkt.pathHashes[0]).toBe(bytesToHex(pathBytes.subarray(0,3))); expect(pkt.pathHashes[0]).toBe(bytesToHex(pathBytes.subarray(0,2)));
}); });
test('unsupported payload type throws', () => { test('unsupported payload type throws', () => {
@@ -231,3 +231,24 @@ describe('Packet decode branches and transport/path parsing', () => {
expect(() => pkt.decode()).toThrow(); expect(() => pkt.decode()).toThrow();
}); });
}); });
describe("Packet.decode overloads", () => {
const ackBytes = new Uint8Array([ /* header */ 13, /* pathLength */ 0, /* payload (4 bytes checksum) */ 1, 2, 3, 4 ]);
test("decode() returns payload only", () => {
const pkt = Packet.fromBytes(ackBytes);
const payload = pkt.decode() as Payload;
expect(payload.type).toBe(PayloadType.ACK);
expect((payload as any).checksum).toEqual(new Uint8Array([1, 2, 3, 4]));
});
test("decode(true) returns { payload, structure }", () => {
const pkt = Packet.fromBytes(ackBytes);
const res = pkt.decode(true) as any;
expect(res).toHaveProperty("payload");
expect(res).toHaveProperty("structure");
expect(res.payload.type).toBe(PayloadType.ACK);
expect(Array.isArray(res.structure)).toBe(true);
expect(res.structure[res.structure.length - 1].name).toBe("ack payload");
});
});