Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
df64e2ef18
|
|||
|
0abdf70658
|
|||
|
e5bf3b1aed
|
|||
|
71a8d05666
|
|||
|
0feb4868e4
|
|||
|
38b7ea7517
|
|||
|
0232da2d95
|
@@ -1,19 +0,0 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
16
eslint.config.ts
Normal file
16
eslint.config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(["dist"]),
|
||||
{
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
extends: [js.configs.recommended, tseslint.configs.recommended],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser
|
||||
}
|
||||
}
|
||||
]);
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@hamradio/meshcore",
|
||||
"type": "module",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.1",
|
||||
"description": "MeshCore protocol support for Typescript",
|
||||
"keywords": [
|
||||
"MeshCore",
|
||||
@@ -15,7 +15,7 @@
|
||||
"license": "MIT",
|
||||
"author": "Wijnand Modderman-Lenstra",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
@@ -23,7 +23,7 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
@@ -49,6 +49,7 @@
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"eslint": "^10.0.3",
|
||||
"globals": "^17.4.0",
|
||||
"jiti": "^2.6.1",
|
||||
"prettier": "3.8.1",
|
||||
"tsup": "^8.5.1",
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
@@ -9,7 +9,7 @@ import { NodeHash } from "./identity.types";
|
||||
const PUBLIC_KEY_SIZE = 32;
|
||||
const SEED_SIZE = 32;
|
||||
const HMAC_SIZE = 2;
|
||||
const SHARED_SECRET_SIZE = 32;
|
||||
const SHARED_SECRET_SIZE = 16;
|
||||
const SIGNATURE_SIZE = 64;
|
||||
const STATIC_SECRET_SIZE = 32;
|
||||
|
||||
@@ -136,28 +136,32 @@ export class SharedSecret implements ISharedSecret {
|
||||
private secret: Uint8Array;
|
||||
|
||||
constructor(secret: Uint8Array) {
|
||||
/*
|
||||
if (secret.length === SHARED_SECRET_SIZE / 2) {
|
||||
// Zero pad to the left if the secret is too short (e.g. from x25519)
|
||||
const padded = new Uint8Array(SHARED_SECRET_SIZE);
|
||||
padded.set(secret, SHARED_SECRET_SIZE - secret.length);
|
||||
padded.set(secret, 0);
|
||||
secret = padded;
|
||||
}
|
||||
*/
|
||||
if (secret.length !== SHARED_SECRET_SIZE) {
|
||||
throw new Error(`Invalid shared secret length: expected ${SHARED_SECRET_SIZE} bytes, got ${secret.length}`);
|
||||
}
|
||||
this.secret = secret;
|
||||
this.secret = new Uint8Array(SHARED_SECRET_SIZE * 2); // Pad to 32 bytes for hashing and encryption
|
||||
this.secret.set(secret, 0);
|
||||
}
|
||||
|
||||
public toHash(): NodeHash {
|
||||
return this.secret[0] as NodeHash;
|
||||
const hash = sha256.create().update(this.secret.slice(0, 16)).digest();
|
||||
return hash[0] as NodeHash;
|
||||
}
|
||||
|
||||
public toBytes(): Uint8Array {
|
||||
return this.secret;
|
||||
return this.secret.slice(0, 16);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return bytesToHex(this.secret);
|
||||
return bytesToHex(this.secret.slice(0, 16));
|
||||
}
|
||||
|
||||
public decrypt(hmac: Uint8Array, ciphertext: Uint8Array): Uint8Array {
|
||||
@@ -242,6 +246,6 @@ export class StaticSecret implements IStaticSecret {
|
||||
|
||||
public diffieHellman(otherPublicKey: IPublicKey): SharedSecret {
|
||||
const sharedSecret = x25519.getSharedSecret(this.secret, otherPublicKey.toBytes());
|
||||
return new SharedSecret(sharedSecret);
|
||||
return new SharedSecret(sharedSecret.slice(0, 16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ export class LocalIdentity extends Identity implements ILocalIdentity {
|
||||
} else {
|
||||
throw new Error("Invalid type for calculateSharedSecret comparison");
|
||||
}
|
||||
return new SharedSecret(this.privateKey.calculateSharedSecret(otherPublicKey));
|
||||
return new SharedSecret(this.privateKey.calculateSharedSecret(otherPublicKey).slice(0, 16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +216,12 @@ export class Contacts {
|
||||
private contacts: Record<number, Contact[]> = {};
|
||||
private groups: Record<number, Group[]> = {};
|
||||
|
||||
constructor() {
|
||||
// These groups are omnipresent:
|
||||
this.addGroup(new Group("Public"));
|
||||
this.addGroup(new Group("#test"));
|
||||
}
|
||||
|
||||
public addLocalIdentity(identity: LocalIdentity) {
|
||||
this.localIdentities.push({ identity, sharedSecrets: {} });
|
||||
}
|
||||
@@ -228,6 +234,30 @@ export class Contacts {
|
||||
this.contacts[hash].push(contact);
|
||||
}
|
||||
|
||||
public hasContact(nameOrHash: string | NodeHash): boolean {
|
||||
if (typeof nameOrHash === "string") {
|
||||
return Object.values(this.contacts)
|
||||
.flat()
|
||||
.some((contact) => contact.name.toLowerCase() === nameOrHash.toLowerCase());
|
||||
} else {
|
||||
const hash = parseNodeHash(nameOrHash) as number;
|
||||
return (this.contacts[hash] || []).length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public getContactByName(name: string): Contact | null {
|
||||
const contact = Object.values(this.contacts)
|
||||
.flat()
|
||||
.find((contact) => contact.name.toLowerCase() === name.toLowerCase());
|
||||
return contact || null;
|
||||
}
|
||||
|
||||
public getContacts(): Contact[] {
|
||||
const contacts = Object.values(this.contacts).flat();
|
||||
contacts.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return contacts;
|
||||
}
|
||||
|
||||
public decrypt(
|
||||
src: NodeHash | PublicKey,
|
||||
dst: NodeHash,
|
||||
@@ -296,13 +326,54 @@ export class Contacts {
|
||||
}
|
||||
|
||||
public addGroup(group: Group) {
|
||||
const hash = parseNodeHash(group.hash()) as number;
|
||||
// Remove any group with the same name (regardless of hash), the hash may
|
||||
// have changed if the secret was changed, but the name is what identifies
|
||||
// the group to the user.
|
||||
for (const key of Object.keys(this.groups)) {
|
||||
const hash = Number(key);
|
||||
this.groups[hash] = this.groups[hash].filter((g) => g.name.toLowerCase() !== group.name.toLowerCase());
|
||||
if (this.groups[hash].length === 0) {
|
||||
delete this.groups[hash];
|
||||
}
|
||||
}
|
||||
|
||||
const hash = group.hash() as number;
|
||||
|
||||
if (!this.groups[hash]) {
|
||||
this.groups[hash] = [];
|
||||
}
|
||||
|
||||
this.groups[hash].push(group);
|
||||
}
|
||||
|
||||
public hasGroup(nameOrHash: string | NodeHash): boolean {
|
||||
if (typeof nameOrHash === "string") {
|
||||
return Object.values(this.groups)
|
||||
.flat()
|
||||
.some((group) => group.name.toLowerCase() === nameOrHash.toLowerCase());
|
||||
} else {
|
||||
const hash = parseNodeHash(nameOrHash) as number;
|
||||
return (this.groups[hash] || []).length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public getGroupByName(name: string): Group | null {
|
||||
const group = Object.values(this.groups)
|
||||
.flat()
|
||||
.find((group) => group.name.toLowerCase() === name.toLowerCase());
|
||||
return group || null;
|
||||
}
|
||||
|
||||
public getGroups(): Group[] {
|
||||
const groups = Object.values(this.groups).flat();
|
||||
groups.sort((a, b) => {
|
||||
if (a.name === "Public") return -1;
|
||||
if (b.name === "Public") return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
|
||||
public decryptGroupText(
|
||||
channelHash: NodeHash,
|
||||
hmac: Uint8Array,
|
||||
@@ -314,7 +385,7 @@ export class Contacts {
|
||||
const hash = parseNodeHash(channelHash) as number;
|
||||
const groups = this.groups[hash] || [];
|
||||
if (groups.length === 0) {
|
||||
throw new Error("Unknown group hash");
|
||||
throw new Error(`Unknown group hash ${hash.toString(16).padStart(2, "0")}`);
|
||||
}
|
||||
for (const group of groups) {
|
||||
try {
|
||||
@@ -324,7 +395,7 @@ export class Contacts {
|
||||
// Ignore decryption errors and try the next group.
|
||||
}
|
||||
}
|
||||
throw new Error("Decryption failed with all known groups");
|
||||
throw new Error(`Decryption failed with all known groups with hash ${hash.toString(16).padStart(2, "0")}`);
|
||||
}
|
||||
|
||||
public decryptGroupData(
|
||||
@@ -338,7 +409,7 @@ export class Contacts {
|
||||
const hash = parseNodeHash(channelHash) as number;
|
||||
const groups = this.groups[hash] || [];
|
||||
if (groups.length === 0) {
|
||||
throw new Error("Unknown group hash");
|
||||
throw new Error(`Unknown group hash ${hash.toString(16).padStart(2, "0")}`);
|
||||
}
|
||||
for (const group of groups) {
|
||||
try {
|
||||
@@ -348,6 +419,6 @@ export class Contacts {
|
||||
// Ignore decryption errors and try the next group.
|
||||
}
|
||||
}
|
||||
throw new Error("Decryption failed with all known groups");
|
||||
throw new Error(`Decryption failed with all known groups with hash ${hash.toString(16).padStart(2, "0")}`);
|
||||
}
|
||||
}
|
||||
|
||||
156
src/packet.ts
156
src/packet.ts
@@ -127,13 +127,13 @@ export class Packet implements IPacket {
|
||||
/* Header segment */
|
||||
{
|
||||
name: "header",
|
||||
data: new Uint8Array([this.header]),
|
||||
data: new Uint8Array([this.header]).buffer,
|
||||
fields: [
|
||||
/* Header flags */
|
||||
{
|
||||
name: "flags",
|
||||
type: FieldType.BITS,
|
||||
size: 1,
|
||||
length: 1,
|
||||
bits: [
|
||||
{ name: "payload version", size: 2 },
|
||||
{ name: "payload type", size: 4 },
|
||||
@@ -153,18 +153,18 @@ export class Packet implements IPacket {
|
||||
this.transport![0] & 0xff,
|
||||
(this.transport![1] >> 8) & 0xff,
|
||||
this.transport![1] & 0xff
|
||||
]),
|
||||
]).buffer,
|
||||
fields: [
|
||||
{
|
||||
name: "transport code 1",
|
||||
type: FieldType.UINT16_BE,
|
||||
size: 2,
|
||||
length: 2,
|
||||
value: this.transport![0]
|
||||
},
|
||||
{
|
||||
name: "transport code 2",
|
||||
type: FieldType.UINT16_BE,
|
||||
size: 2,
|
||||
length: 2,
|
||||
value: this.transport![1]
|
||||
}
|
||||
]
|
||||
@@ -175,12 +175,12 @@ export class Packet implements IPacket {
|
||||
/* Path length and hashes */
|
||||
{
|
||||
name: "path",
|
||||
data: new Uint8Array([this.pathLength, ...this.path]),
|
||||
data: new Uint8Array([this.pathLength, ...this.path]).buffer,
|
||||
fields: [
|
||||
{
|
||||
name: "path length",
|
||||
type: FieldType.UINT8,
|
||||
size: 1,
|
||||
length: 1,
|
||||
bits: [
|
||||
{ name: "path hash size", size: 2 },
|
||||
{ name: "path hash count", size: 6 }
|
||||
@@ -189,7 +189,7 @@ export class Packet implements IPacket {
|
||||
{
|
||||
name: "path hashes",
|
||||
type: pathHashType,
|
||||
size: this.path.length
|
||||
length: this.path.length
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -269,14 +269,19 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "request payload",
|
||||
data: this.payload,
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
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 }
|
||||
{ name: "destination hash", type: FieldType.UINT8, length: 1, value: dst },
|
||||
{ name: "source hash", type: FieldType.UINT8, length: 1, value: src },
|
||||
{ name: "cipher MAC", type: FieldType.BYTES, length: 2, value: encrypted.cipherMAC },
|
||||
{
|
||||
name: "cipher text",
|
||||
type: FieldType.BYTES,
|
||||
length: encrypted.cipherText.length,
|
||||
value: encrypted.cipherText
|
||||
}
|
||||
]
|
||||
};
|
||||
return { payload, segment };
|
||||
@@ -301,14 +306,19 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "response payload",
|
||||
data: this.payload,
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
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 }
|
||||
{ name: "destination hash", type: FieldType.UINT8, length: 1, value: dst },
|
||||
{ name: "source hash", type: FieldType.UINT8, length: 1, value: src },
|
||||
{ name: "cipher MAC", type: FieldType.BYTES, length: 2, value: encrypted.cipherMAC },
|
||||
{
|
||||
name: "cipher text",
|
||||
type: FieldType.BYTES,
|
||||
length: encrypted.cipherText.length,
|
||||
value: encrypted.cipherText
|
||||
}
|
||||
]
|
||||
};
|
||||
return { payload, segment };
|
||||
@@ -333,14 +343,19 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "text payload",
|
||||
data: this.payload,
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
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 }
|
||||
{ name: "destination hash", type: FieldType.UINT8, length: 1, value: dst },
|
||||
{ name: "source hash", type: FieldType.UINT8, length: 1, value: src },
|
||||
{ name: "cipher MAC", type: FieldType.BYTES, length: 2, value: encrypted.cipherMAC },
|
||||
{
|
||||
name: "cipher text",
|
||||
type: FieldType.BYTES,
|
||||
length: encrypted.cipherText.length,
|
||||
value: encrypted.cipherText
|
||||
}
|
||||
]
|
||||
};
|
||||
return { payload, segment };
|
||||
@@ -361,10 +376,10 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "ack payload",
|
||||
data: this.payload,
|
||||
fields: [{ name: "checksum", type: FieldType.BYTES, size: 4, value: checksum }]
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
fields: [{ name: "checksum", type: FieldType.BYTES, length: 4, value: checksum }]
|
||||
};
|
||||
return { payload, segment };
|
||||
}
|
||||
@@ -388,11 +403,11 @@ export class Packet implements IPacket {
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
segment = {
|
||||
name: "advert payload",
|
||||
data: this.payload,
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
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 }
|
||||
{ type: FieldType.BYTES, name: "public key", length: 32 },
|
||||
{ type: FieldType.UINT32_LE, name: "timestamp", length: 4, value: payload.timestamp! },
|
||||
{ type: FieldType.BYTES, name: "signature", length: 64 }
|
||||
]
|
||||
};
|
||||
}
|
||||
@@ -409,7 +424,7 @@ export class Packet implements IPacket {
|
||||
segment!.fields.push({
|
||||
type: FieldType.BITS,
|
||||
name: "flags",
|
||||
size: 1,
|
||||
length: 1,
|
||||
value: flags,
|
||||
bits: [
|
||||
{ size: 1, name: "name flag" },
|
||||
@@ -426,20 +441,20 @@ export class Packet implements IPacket {
|
||||
const lon = reader.int32() / 1000000;
|
||||
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 });
|
||||
segment!.fields.push({ type: FieldType.UINT32_LE, name: "latitude", length: 4, value: lat });
|
||||
segment!.fields.push({ type: FieldType.UINT32_LE, name: "longitude", length: 4, value: lon });
|
||||
}
|
||||
}
|
||||
if (appdata.hasFeature1) {
|
||||
appdata.feature1 = reader.uint16();
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
segment!.fields.push({ type: FieldType.UINT16_LE, name: "feature1", size: 2, value: appdata.feature1 });
|
||||
segment!.fields.push({ type: FieldType.UINT16_LE, name: "feature1", length: 2, value: appdata.feature1 });
|
||||
}
|
||||
}
|
||||
if (appdata.hasFeature2) {
|
||||
appdata.feature2 = reader.uint16();
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
segment!.fields.push({ type: FieldType.UINT16_LE, name: "feature2", size: 2, value: appdata.feature2 });
|
||||
segment!.fields.push({ type: FieldType.UINT16_LE, name: "feature2", length: 2, value: appdata.feature2 });
|
||||
}
|
||||
}
|
||||
if (appdata.hasName) {
|
||||
@@ -448,7 +463,7 @@ export class Packet implements IPacket {
|
||||
segment!.fields.push({
|
||||
type: FieldType.C_STRING,
|
||||
name: "name",
|
||||
size: appdata.name.length,
|
||||
length: appdata.name.length,
|
||||
value: appdata.name
|
||||
});
|
||||
}
|
||||
@@ -476,13 +491,18 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "group text payload",
|
||||
data: this.payload,
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
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 }
|
||||
{ name: "channel hash", type: FieldType.UINT8, length: 1, value: channelHash },
|
||||
{ name: "cipher MAC", type: FieldType.BYTES, length: 2, value: encrypted.cipherMAC },
|
||||
{
|
||||
name: "cipher text",
|
||||
type: FieldType.BYTES,
|
||||
length: encrypted.cipherText.length,
|
||||
value: encrypted.cipherText
|
||||
}
|
||||
]
|
||||
};
|
||||
return { payload, segment };
|
||||
@@ -503,16 +523,16 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "group data payload",
|
||||
data: this.payload,
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
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: "channel hash", type: FieldType.UINT8, length: 1, value: payload.channelHash },
|
||||
{ name: "cipher MAC", type: FieldType.BYTES, length: 2, value: payload.encrypted.cipherMAC },
|
||||
{
|
||||
name: "cipher text",
|
||||
type: FieldType.BYTES,
|
||||
size: payload.encrypted.cipherText.length,
|
||||
length: payload.encrypted.cipherText.length,
|
||||
value: payload.encrypted.cipherText
|
||||
}
|
||||
]
|
||||
@@ -536,17 +556,17 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "anon req payload",
|
||||
data: this.payload,
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
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: "destination hash", type: FieldType.UINT8, length: 1, value: payload.dst },
|
||||
{ name: "public key", type: FieldType.BYTES, length: 32, value: payload.publicKey },
|
||||
{ name: "cipher MAC", type: FieldType.BYTES, length: 2, value: payload.encrypted.cipherMAC },
|
||||
{
|
||||
name: "cipher text",
|
||||
type: FieldType.BYTES,
|
||||
size: payload.encrypted.cipherText.length,
|
||||
length: payload.encrypted.cipherText.length,
|
||||
value: payload.encrypted.cipherText
|
||||
}
|
||||
]
|
||||
@@ -569,12 +589,12 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "path payload",
|
||||
data: this.payload,
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
fields: [
|
||||
{ name: "destination hash", type: FieldType.UINT8, size: 1, value: payload.dst },
|
||||
{ name: "source hash", type: FieldType.UINT8, size: 1, value: payload.src }
|
||||
{ name: "destination hash", type: FieldType.UINT8, length: 1, value: payload.dst },
|
||||
{ name: "source hash", type: FieldType.UINT8, length: 1, value: payload.src }
|
||||
]
|
||||
};
|
||||
return { payload, segment };
|
||||
@@ -597,14 +617,14 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "trace payload",
|
||||
data: this.payload,
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
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 }
|
||||
{ name: "tag", type: FieldType.DWORDS, length: 4, value: payload.tag },
|
||||
{ name: "auth code", type: FieldType.DWORDS, length: 4, value: payload.authCode },
|
||||
{ name: "flags", type: FieldType.UINT8, length: 1, value: payload.flags },
|
||||
{ name: "nodes", type: FieldType.BYTES, length: payload.nodes.length, value: payload.nodes }
|
||||
]
|
||||
};
|
||||
return { payload, segment };
|
||||
@@ -619,10 +639,10 @@ export class Packet implements IPacket {
|
||||
};
|
||||
|
||||
if (typeof withSegment === "boolean" && withSegment) {
|
||||
const segment = {
|
||||
const segment: Segment = {
|
||||
name: "raw custom payload",
|
||||
data: this.payload,
|
||||
fields: [{ name: "data", type: FieldType.BYTES, size: this.payload.length, value: this.payload }]
|
||||
data: new Uint8Array(this.payload).buffer,
|
||||
fields: [{ name: "data", type: FieldType.BYTES, length: this.payload.length, value: this.payload }]
|
||||
};
|
||||
return { payload, segment };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Dissected } from "@hamradio/packet";
|
||||
import { NodeHash } from "./identity.types";
|
||||
import { Contact, Group } from "./identity";
|
||||
|
||||
// IPacket contains the raw packet bytes.
|
||||
export type Uint16 = number; // 0..65535
|
||||
@@ -67,6 +68,7 @@ export interface RequestPayload extends BasePayload {
|
||||
src: NodeHash;
|
||||
encrypted: EncryptedPayload;
|
||||
decrypted?: DecryptedRequest;
|
||||
contact?: Contact;
|
||||
}
|
||||
|
||||
export enum RequestType {
|
||||
@@ -91,6 +93,7 @@ export interface ResponsePayload extends BasePayload {
|
||||
src: NodeHash;
|
||||
encrypted: EncryptedPayload;
|
||||
decrypted?: DecryptedResponse;
|
||||
contact?: Contact;
|
||||
}
|
||||
|
||||
export interface DecryptedResponse {
|
||||
@@ -104,6 +107,7 @@ export interface TextPayload extends BasePayload {
|
||||
src: NodeHash;
|
||||
encrypted: EncryptedPayload;
|
||||
decrypted?: DecryptedText;
|
||||
contact?: Contact;
|
||||
}
|
||||
|
||||
export enum TextType {
|
||||
@@ -163,6 +167,7 @@ export interface GroupTextPayload extends BasePayload {
|
||||
channelHash: NodeHash;
|
||||
encrypted: EncryptedPayload;
|
||||
decrypted?: DecryptedGroupText;
|
||||
group?: Group;
|
||||
}
|
||||
|
||||
export interface DecryptedGroupText {
|
||||
@@ -177,6 +182,7 @@ export interface GroupDataPayload extends BasePayload {
|
||||
channelHash: NodeHash;
|
||||
encrypted: EncryptedPayload;
|
||||
decrypted?: DecryptedGroupData;
|
||||
group?: Group;
|
||||
}
|
||||
|
||||
export interface DecryptedGroupData {
|
||||
@@ -190,6 +196,7 @@ export interface AnonReqPayload extends BasePayload {
|
||||
publicKey: Uint8Array;
|
||||
encrypted: EncryptedPayload;
|
||||
decrypted?: DecryptedAnonReq;
|
||||
contact?: Contact;
|
||||
}
|
||||
|
||||
export interface DecryptedAnonReq {
|
||||
|
||||
@@ -112,18 +112,12 @@ describe("PrivateKey", () => {
|
||||
});
|
||||
|
||||
describe("SharedSecret", () => {
|
||||
const secret = randomBytes(32);
|
||||
const secret = randomBytes(16);
|
||||
|
||||
it("constructs from 32 bytes", () => {
|
||||
it("constructs from 16 bytes", () => {
|
||||
const ss = new SharedSecret(secret);
|
||||
expect(ss.toBytes()).toEqual(secret);
|
||||
});
|
||||
|
||||
it("pads if 16 bytes", () => {
|
||||
const short = randomBytes(16);
|
||||
const ss = new SharedSecret(short);
|
||||
expect(ss.toBytes().length).toBe(32);
|
||||
expect(Array.from(ss.toBytes()).slice(16)).toEqual(Array.from(short));
|
||||
expect(ss.toBytes().length).toBe(16);
|
||||
expect(bytesToHex(ss.toBytes())).toBe(bytesToHex(secret));
|
||||
});
|
||||
|
||||
it("throws on invalid length", () => {
|
||||
@@ -163,6 +157,9 @@ describe("SharedSecret", () => {
|
||||
it('fromName "Public"', () => {
|
||||
const ss = SharedSecret.fromName("Public");
|
||||
expect(ss).toBeInstanceOf(SharedSecret);
|
||||
const hash = ss.toHash();
|
||||
expect(typeof hash).toBe("number");
|
||||
expect(hash).toBe(0x11);
|
||||
});
|
||||
|
||||
it("fromName with #group", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { bytesToHex } from "@hamradio/packet";
|
||||
import { bytesToHex, hexToBytes } from "@hamradio/packet";
|
||||
import { Identity, LocalIdentity, Contact, Group, Contacts, parseNodeHash } from "../src/identity";
|
||||
import { PrivateKey, PublicKey, SharedSecret } from "../src/crypto";
|
||||
import { DecryptedGroupText, DecryptedGroupData } from "../src/packet.types";
|
||||
@@ -230,6 +230,18 @@ describe("Group", () => {
|
||||
it("decryptData throws on short ciphertext", () => {
|
||||
expect(() => group.decryptData(randomBytes(16), Uint8Array.of(1, 2, 3))).toThrow();
|
||||
});
|
||||
|
||||
it("hash is consistent with test vectors", () => {
|
||||
const testVectors = [
|
||||
{ name: "Public", hash: 0x11 },
|
||||
{ name: "#test", hash: 0xd9 }
|
||||
];
|
||||
|
||||
testVectors.forEach(({ name, hash }) => {
|
||||
const g = new Group(name);
|
||||
expect(`${name}:${g.hash()}`).toBe(`${name}:${hash}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Contacts", () => {
|
||||
@@ -501,4 +513,23 @@ describe("Contacts", () => {
|
||||
const res2 = contacts.decrypt(contactB.identity.hash(), localA.publicKey.key[0], hmac, ciphertext);
|
||||
expect(res2.decrypted).toEqual(msg);
|
||||
});
|
||||
|
||||
it("decryptGroupText on well known channels with test vectors", () => {
|
||||
const testVectors = [
|
||||
{
|
||||
name: "#test",
|
||||
channelHash: 0xd9,
|
||||
cipherMAC: hexToBytes("570D"),
|
||||
cipherText: hexToBytes("E397F0560B2B61396F7E236811FC70B70038E956045347D7F6B9976A46727427"),
|
||||
message: "corrauder 🏕️: Testing "
|
||||
}
|
||||
];
|
||||
|
||||
testVectors.forEach(({ name, channelHash, cipherMAC, cipherText, message }) => {
|
||||
const group = new Group(name);
|
||||
expect(group.hash()).toBe(channelHash);
|
||||
const decrypted = group.decryptText(cipherMAC, cipherText);
|
||||
expect(decrypted.message).toBe(message);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user