Initial import

This commit is contained in:
2026-03-09 22:05:39 +01:00
parent 73631969c9
commit 344c89a8d0
15 changed files with 3894 additions and 10 deletions

331
src/identity.ts Normal file
View File

@@ -0,0 +1,331 @@
import { ecb } from '@noble/ciphers/aes.js';
import { hmac } from '@noble/hashes/hmac.js';
import { sha256 } from "@noble/hashes/sha2.js";
import { ed25519, x25519 } from '@noble/curves/ed25519.js';
import {
BaseGroup,
BaseGroupSecret,
BaseIdentity,
BaseKeyManager,
BaseLocalIdentity,
Contact,
DecryptedAnonReq,
DecryptedGroupData,
DecryptedGroupText,
DecryptedRequest,
DecryptedResponse,
DecryptedText,
EncryptedPayload,
NodeHash,
Secret
} from "./types";
import { BufferReader, bytesToHex, equalBytes, hexToBytes } from "./parser";
// The "Public" group is a special group that all nodes are implicitly part of. It uses a fixed secret derived from the string "Public".
const publicSecret = hexToBytes("8b3387e9c5cdea6ac9e5edbaa115cd72");
export class Identity extends BaseIdentity {
constructor(publicKey: Uint8Array | string) {
if (typeof publicKey === "string") {
publicKey = hexToBytes(publicKey);
}
super(publicKey);
}
public hash(): NodeHash {
return bytesToHex(this.publicKey.slice(0, 1));
}
public verify(message: Uint8Array, signature: Uint8Array): boolean {
return ed25519.verify(message, signature, this.publicKey);
}
}
export class LocalIdentity extends Identity implements BaseLocalIdentity {
public privateKey: Uint8Array;
constructor(seed: Uint8Array | string) {
if (typeof seed === "string") {
seed = hexToBytes(seed);
}
const { secretKey, publicKey } = ed25519.keygen(seed);
super(publicKey);
this.privateKey = secretKey;
}
public sign(message: Uint8Array): Uint8Array {
return ed25519.sign(message, this.privateKey);
}
public calculateSharedSecret(other: BaseIdentity | Uint8Array): Uint8Array {
if (other instanceof Uint8Array) {
return x25519.getSharedSecret(this.privateKey, other);
}
return x25519.getSharedSecret(this.privateKey, other.publicKey);
}
public hash(): NodeHash {
return super.hash();
}
public verify(message: Uint8Array, signature: Uint8Array): boolean {
return super.verify(message, signature);
}
}
export class Group extends BaseGroup {}
export class GroupSecret extends BaseGroupSecret {
public static fromName(name: string): GroupSecret {
if (name === "Public") {
return new GroupSecret(publicSecret);
} else if (!/^#/.test(name)) {
throw new Error("Only the 'Public' group or groups starting with '#' are supported");
}
const hash = sha256.create().update(new TextEncoder().encode(name)).digest();
return new GroupSecret(hash.slice(0, 16));
}
public hash(): NodeHash {
return bytesToHex(this.secret.slice(0, 1));
}
public decryptText(encrypted: EncryptedPayload): DecryptedGroupText {
const data = this.macThenDecrypt(encrypted.cipherMAC, encrypted.cipherText);
if (data.length < 8) {
throw new Error("Invalid ciphertext");
}
const reader = new BufferReader(data);
const timestamp = reader.readTimestamp();
const flags = reader.readByte();
const textType = (flags >> 2) & 0x3F;
const attempt = flags & 0x03;
const message = new TextDecoder('utf-8').decode(reader.readBytes());
return {
timestamp,
textType,
attempt,
message
}
}
public decryptData(encrypted: EncryptedPayload): DecryptedGroupData {
const data = this.macThenDecrypt(encrypted.cipherMAC, encrypted.cipherText);
if (data.length < 8) {
throw new Error("Invalid ciphertext");
}
const reader = new BufferReader(data);
return {
timestamp: reader.readTimestamp(),
data: reader.readBytes(reader.remainingBytes())
};
}
private macThenDecrypt(cipherMAC: Uint8Array, cipherText: Uint8Array): Uint8Array {
const mac = hmac(sha256, this.secret, cipherText);
if (!equalBytes(mac, cipherMAC)) {
throw new Error("Invalid MAC");
}
const block = ecb(this.secret.slice(0, 16), { disablePadding: true });
const plain = block.decrypt(cipherText);
return plain;
}
}
export class KeyManager extends BaseKeyManager {
private groups: Map<NodeHash, Group[]> = new Map();
private contacts: Map<NodeHash, Contact[]> = new Map();
private localIdentities: Map<NodeHash, LocalIdentity[]> = new Map();
public addGroup(group: Group): void {
const hash = group.secret.hash();
if (!this.groups.has(hash)) {
this.groups.set(hash, [group]);
} else {
this.groups.get(hash)!.push(group);
}
}
public addGroupSecret(name: string, secret?: Secret): void {
if (typeof secret === "undefined") {
secret = GroupSecret.fromName(name).secret;
} else if (typeof secret === "string") {
secret = hexToBytes(secret);
}
this.addGroup(new Group(name, new GroupSecret(secret)));
}
public decryptGroupText(channelHash: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedGroupText, group: Group } {
const groupSecrets = this.groups.get(channelHash);
if (!groupSecrets) {
throw new Error("No group secrets for channel");
}
for (const group of groupSecrets) {
try {
const decrypted = group.decryptText(encrypted);
return { decrypted, group: group };
} catch (e) {
// Ignore and try next secret
}
}
throw new Error("Failed to decrypt group text with any known secret");
}
public decryptGroupData(channelHash: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedGroupData, group: Group } {
const groupSecrets = this.groups.get(channelHash);
if (!groupSecrets) {
throw new Error("No group secrets for channel");
}
for (const group of groupSecrets) {
try {
const decrypted = group.decryptData(encrypted);
return { decrypted, group };
} catch (e) {
// Ignore and try next secret
}
}
throw new Error("Failed to decrypt group data with any known secret");
}
public addContact(contact: Contact): void {
const hash = bytesToHex(contact.publicKey.slice(0, 1));
if (!this.contacts.has(hash)) {
this.contacts.set(hash, [contact]);
} else {
this.contacts.get(hash)!.push(contact);
}
}
public addIdentity(name: string, publicKey: Uint8Array | string): void {
if (typeof publicKey === "string") {
publicKey = hexToBytes(publicKey);
}
this.addContact({ name, publicKey });
}
public addLocalIdentity(seed: Secret): void {
const localIdentity = new LocalIdentity(seed);
const hash = localIdentity.hash();
if (!this.localIdentities.has(hash)) {
this.localIdentities.set(hash, [localIdentity]);
} else {
this.localIdentities.get(hash)!.push(localIdentity);
}
}
private tryDecrypt(dst: NodeHash, src: NodeHash, encrypted: EncryptedPayload): { decrypted: Uint8Array, contact: Contact, identity: BaseIdentity } {
if (!this.localIdentities.has(dst)) {
throw new Error(`No local identities for destination ${dst}`);
}
const localIdentities = this.localIdentities.get(dst)!;
if (!this.contacts.has(src)) {
throw new Error(`No contacts for source ${src}`);
}
const contacts = this.contacts.get(src)!;
for (const localIdentity of localIdentities) {
for (const contact of contacts) {
const sharedSecret = localIdentity.calculateSharedSecret(contact.publicKey);
const mac = hmac(sha256, sharedSecret, encrypted.cipherText);
if (!equalBytes(mac, encrypted.cipherMAC)) {
continue; // Invalid MAC, try next combination
}
const block = ecb(sharedSecret.slice(0, 16), { disablePadding: true });
const plain = block.decrypt(encrypted.cipherText);
if (plain.length < 8) {
continue; // Invalid plaintext, try next combination
}
return { decrypted: plain, contact, identity: localIdentity };
}
}
throw new Error("Failed to decrypt with any known identity/contact combination");
}
public decryptRequest(dst: NodeHash, src: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedRequest, contact: Contact, identity: BaseIdentity } {
const { decrypted, contact, identity } = this.tryDecrypt(dst, src, encrypted);
const reader = new BufferReader(decrypted);
return {
decrypted: {
timestamp: reader.readTimestamp(),
requestType: reader.readByte(),
requestData: reader.readBytes(),
},
contact,
identity
}
}
public decryptResponse(dst: NodeHash, src: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedResponse, contact: Contact, identity: BaseIdentity } {
const { decrypted, contact, identity } = this.tryDecrypt(dst, src, encrypted);
const reader = new BufferReader(decrypted);
return {
decrypted: {
timestamp: reader.readTimestamp(),
responseData: reader.readBytes(),
},
contact,
identity
}
}
public decryptText(dst: NodeHash, src: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedText, contact: Contact, identity: BaseIdentity } {
const { decrypted, contact, identity } = this.tryDecrypt(dst, src, encrypted);
const reader = new BufferReader(decrypted);
const timestamp = reader.readTimestamp();
const flags = reader.readByte();
const textType = (flags >> 2) & 0x3F;
const attempt = flags & 0x03;
const message = new TextDecoder('utf-8').decode(reader.readBytes());
return {
decrypted: {
timestamp,
textType,
attempt,
message
},
contact,
identity
}
}
public decryptAnonReq(dst: NodeHash, publicKey: Uint8Array, encrypted: EncryptedPayload): { decrypted: DecryptedAnonReq, contact: Contact, identity: BaseIdentity } {
if (!this.localIdentities.has(dst)) {
throw new Error(`No local identities for destination ${dst}`);
}
const localIdentities = this.localIdentities.get(dst)!;
const contact = { publicKey } as Contact; // Create a temporary contact object for MAC verification
for (const localIdentity of localIdentities) {
const sharedSecret = localIdentity.calculateSharedSecret(publicKey);
const mac = hmac(sha256, sharedSecret, encrypted.cipherText);
if (!equalBytes(mac, encrypted.cipherMAC)) {
continue; // Invalid MAC, try next identity
}
const block = ecb(sharedSecret.slice(0, 16), { disablePadding: true });
const plain = block.decrypt(encrypted.cipherText);
if (plain.length < 8) {
continue; // Invalid plaintext, try next identity
}
const reader = new BufferReader(plain);
return {
decrypted: {
timestamp: reader.readTimestamp(),
data: reader.readBytes(),
},
contact,
identity: localIdentity
}
}
throw new Error("Failed to decrypt anonymous request with any known identity");
}
}

11
src/index.ts Normal file
View File

@@ -0,0 +1,11 @@
import { Packet } from "./packet";
import {
RouteType,
PayloadType,
} from "./types";
export default {
Packet,
RouteType,
PayloadType,
};

313
src/packet.ts Normal file
View File

@@ -0,0 +1,313 @@
import { sha256 } from "@noble/hashes/sha2.js";
import {
AckPayload,
AdvertAppData,
AdvertPayload,
AnonReqPayload,
EncryptedPayload,
GroupDataPayload,
GroupTextPayload,
PathPayload,
Payload,
PayloadType,
RawCustomPayload,
RequestPayload,
ResponsePayload,
RouteType,
TextPayload,
TracePayload,
type IPacket,
type NodeHash
} from "./types";
import {
base64ToBytes,
BufferReader,
bytesToHex
} from "./parser";
export class Packet implements IPacket {
// Raw packet bytes.
public header: number;
public pathLength: number;
public path: Uint8Array;
public payload: Uint8Array;
// Parsed packet fields.
public transport?: [number, number];
public routeType: RouteType;
public payloadVersion: number;
public payloadType: PayloadType;
public pathHashCount: number;
public pathHashSize: number;
public pathHashBytes: number;
public pathHashes: NodeHash[];
constructor(header: number, transport: [number, number] | undefined, pathLength: number, path: Uint8Array, payload: Uint8Array) {
this.header = header;
this.transport = transport;
this.pathLength = pathLength;
this.path = path;
this.payload = payload;
this.routeType = (header) & 0x03;
this.payloadVersion = (header >> 6) & 0x03;
this.payloadType = (header >> 2) & 0x0f;
this.pathHashCount = (pathLength >> 6) + 1;
this.pathHashSize = pathLength & 0x3f;
this.pathHashBytes = this.pathHashCount * this.pathHashSize;
this.pathHashes = [];
for (let i = 0; i < this.pathHashCount; i++) {
const hashBytes = this.path.slice(i * this.pathHashSize, (i + 1) * this.pathHashSize);
const hashHex = bytesToHex(hashBytes);
this.pathHashes.push(hashHex);
}
}
public static fromBytes(bytes: Uint8Array | string): Packet {
if (typeof bytes === "string") {
bytes = base64ToBytes(bytes);
}
let offset: number = 0;
const header = bytes[offset++];
const routeType = header & 0x03;
let transport: [number, number] | undefined;
if (Packet.hasTransportCodes(routeType)) {
const uitn16View = new DataView(bytes.buffer, bytes.byteOffset + offset, 4);
transport = [uitn16View.getUint16(0, false), uitn16View.getUint16(2, false)];
offset += 4;
}
const pathLength = bytes[offset++];
const path = bytes.slice(offset, offset + pathLength);
offset += pathLength;
const payload = bytes.slice(offset);
return new Packet(header, transport, pathLength, path, payload);
}
public static hasTransportCodes(routeType: RouteType): boolean {
return routeType === RouteType.TRANSPORT_FLOOD || routeType === RouteType.TRANSPORT_DIRECT;
}
public hash(): string {
const hash = sha256.create();
hash.update(new Uint8Array([this.payloadType]));
if (this.payloadType === PayloadType.TRACE) {
hash.update(new Uint8Array([this.pathLength]));
}
hash.update(this.payload);
const digest = hash.digest();
return bytesToHex(digest.slice(0, 8));
}
public decode(): Payload {
switch (this.payloadType) {
case PayloadType.REQUEST:
return this.decodeRequest();
case PayloadType.RESPONSE:
return this.decodeResponse();
case PayloadType.TEXT:
return this.decodeText();
case PayloadType.ACK:
return this.decodeAck();
case PayloadType.ADVERT:
return this.decodeAdvert();
case PayloadType.GROUP_TEXT:
return this.decodeGroupText();
case PayloadType.GROUP_DATA:
return this.decodeGroupData();
case PayloadType.ANON_REQ:
return this.decodeAnonReq();
case PayloadType.PATH:
return this.decodePath();
case PayloadType.TRACE:
return this.decodeTrace();
case PayloadType.RAW_CUSTOM:
return this.decodeRawCustom();
default:
throw new Error(`Unsupported payload type: ${this.payloadType}`);
}
}
private decodeEncryptedPayload(reader: BufferReader): EncryptedPayload {
const cipherMAC = reader.readBytes(2);
const cipherText = reader.readBytes(reader.remainingBytes());
return { cipherMAC, cipherText };
}
private decodeRequest(): RequestPayload {
if (this.payload.length < 4) {
throw new Error("Invalid request payload: too short");
}
const reader = new BufferReader(this.payload);
return {
type: PayloadType.REQUEST,
dst: bytesToHex(reader.readBytes(1)),
src: bytesToHex(reader.readBytes(1)),
encrypted: this.decodeEncryptedPayload(reader),
}
}
private decodeResponse(): ResponsePayload {
if (this.payload.length < 4) {
throw new Error("Invalid response payload: too short");
}
const reader = new BufferReader(this.payload);
return {
type: PayloadType.RESPONSE,
dst: bytesToHex(reader.readBytes(1)),
src: bytesToHex(reader.readBytes(1)),
encrypted: this.decodeEncryptedPayload(reader),
}
}
private decodeText(): TextPayload {
if (this.payload.length < 4) {
throw new Error("Invalid text payload: too short");
}
const reader = new BufferReader(this.payload);
return {
type: PayloadType.TEXT,
dst: bytesToHex(reader.readBytes(1)),
src: bytesToHex(reader.readBytes(1)),
encrypted: this.decodeEncryptedPayload(reader),
}
}
private decodeAck(): AckPayload {
if (this.payload.length < 4) {
throw new Error("Invalid ack payload: too short");
}
const reader = new BufferReader(this.payload);
return {
type: PayloadType.ACK,
checksum: reader.readBytes(4),
}
}
private decodeAdvert(): AdvertPayload {
if (this.payload.length < 4) {
throw new Error("Invalid advert payload: too short");
}
const reader = new BufferReader(this.payload);
let payload: Partial<AdvertPayload> = {
type: PayloadType.ADVERT,
publicKey: reader.readBytes(32),
timestamp: reader.readTimestamp(),
signature: reader.readBytes(64),
}
const flags = reader.readByte();
let appdata: AdvertAppData = {
nodeType: flags & 0x0f,
hasLocation: (flags & 0x10) !== 0,
hasFeature1: (flags & 0x20) !== 0,
hasFeature2: (flags & 0x40) !== 0,
hasName: (flags & 0x80) !== 0,
}
if (appdata.hasLocation) {
const lat = reader.readInt32LE() / 100000;
const lon = reader.readInt32LE() / 100000;
appdata.location = [lat, lon];
}
if (appdata.hasFeature1) {
appdata.feature1 = reader.readUint16LE();
}
if (appdata.hasFeature2) {
appdata.feature2 = reader.readUint16LE();
}
if (appdata.hasName) {
const nameBytes = reader.readBytes();
let nullPos = nameBytes.indexOf(0);
if (nullPos === -1) {
nullPos = nameBytes.length;
}
appdata.name = new TextDecoder('utf-8').decode(nameBytes.subarray(0, nullPos));
}
return {
...payload,
appdata
} as AdvertPayload;
}
private decodeGroupText(): GroupTextPayload {
if (this.payload.length < 3) {
throw new Error("Invalid group text payload: too short");
}
const reader = new BufferReader(this.payload);
return {
type: PayloadType.GROUP_TEXT,
channelHash: bytesToHex(reader.readBytes(1)),
encrypted: this.decodeEncryptedPayload(reader),
}
}
private decodeGroupData(): GroupDataPayload {
if (this.payload.length < 3) {
throw new Error("Invalid group data payload: too short");
}
const reader = new BufferReader(this.payload);
return {
type: PayloadType.GROUP_DATA,
channelHash: bytesToHex(reader.readBytes(1)),
encrypted: this.decodeEncryptedPayload(reader),
}
}
private decodeAnonReq(): AnonReqPayload {
if (this.payload.length < 1 + 32 + 2) {
throw new Error("Invalid anon req payload: too short");
}
const reader = new BufferReader(this.payload);
return {
type: PayloadType.ANON_REQ,
dst: bytesToHex(reader.readBytes(1)),
publicKey: reader.readBytes(32),
encrypted: this.decodeEncryptedPayload(reader),
}
}
private decodePath(): PathPayload {
if (this.payload.length < 2) {
throw new Error("Invalid path payload: too short");
}
const reader = new BufferReader(this.payload);
return {
type: PayloadType.PATH,
dst: bytesToHex(reader.readBytes(1)),
src: bytesToHex(reader.readBytes(1)),
}
}
private decodeTrace(): TracePayload {
if (this.payload.length < 9) {
throw new Error("Invalid trace payload: too short");
}
const reader = new BufferReader(this.payload);
return {
type: PayloadType.TRACE,
tag: reader.readUint32LE() >>> 0,
authCode: reader.readUint32LE() >>> 0,
flags: reader.readByte() & 0x03,
nodes: reader.readBytes()
}
}
private decodeRawCustom(): RawCustomPayload {
return {
type: PayloadType.RAW_CUSTOM,
data: this.payload,
}
}
}

81
src/parser.ts Normal file
View File

@@ -0,0 +1,81 @@
import { equalBytes } from "@noble/ciphers/utils.js";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
export {
bytesToHex,
hexToBytes,
equalBytes
};
export const base64ToBytes = (base64: string): Uint8Array => {
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
export class BufferReader {
private buffer: Uint8Array;
private offset: number;
constructor(buffer: Uint8Array) {
this.buffer = buffer;
this.offset = 0;
}
public readByte(): number {
return this.buffer[this.offset++];
}
public readBytes(length?: number): Uint8Array {
if (length === undefined) {
length = this.buffer.length - this.offset;
}
const bytes = this.buffer.slice(this.offset, this.offset + length);
this.offset += length;
return bytes;
}
public hasMore(): boolean {
return this.offset < this.buffer.length;
}
public remainingBytes(): number {
return this.buffer.length - this.offset;
}
public peekByte(): number {
return this.buffer[this.offset];
}
public readUint16LE(): number {
const value = this.buffer[this.offset] | (this.buffer[this.offset + 1] << 8);
this.offset += 2;
return value;
}
public readUint32LE(): number {
const value = this.buffer[this.offset] | (this.buffer[this.offset + 1] << 8) | (this.buffer[this.offset + 2] << 16) | (this.buffer[this.offset + 3] << 24);
this.offset += 4;
return value;
}
public readInt16LE(): number {
const value = this.buffer[this.offset] | (this.buffer[this.offset + 1] << 8);
this.offset += 2;
return value < 0x8000 ? value : value - 0x10000;
}
public readInt32LE(): number {
const value = this.buffer[this.offset] | (this.buffer[this.offset + 1] << 8) | (this.buffer[this.offset + 2] << 16) | (this.buffer[this.offset + 3] << 24);
this.offset += 4;
return value < 0x80000000 ? value : value - 0x100000000;
}
public readTimestamp(): Date {
const timestamp = this.readUint32LE();
return new Date(timestamp * 1000);
}
}

296
src/types.ts Normal file
View File

@@ -0,0 +1,296 @@
import { equalBytes, hexToBytes } from "./parser";
// IPacket contains the raw packet bytes.
export type Uint16 = number; // 0..65535
/* Packet types and structures. */
export interface IPacket {
header: number;
transport?: [Uint16, Uint16];
pathLength: number;
path: Uint8Array;
payload: Uint8Array;
decode(): Payload;
}
export enum RouteType {
TRANSPORT_FLOOD = 0,
FLOOD = 1,
DIRECT = 2,
TRANSPORT_DIRECT = 3
}
export enum PayloadType {
REQUEST = 0x00,
RESPONSE = 0x01,
TEXT = 0x02,
ACK = 0x03,
ADVERT = 0x04,
GROUP_TEXT = 0x05,
GROUP_DATA = 0x06,
ANON_REQ = 0x07,
PATH = 0x08,
TRACE = 0x09,
RAW_CUSTOM = 0x0f,
}
export type Payload =
| RequestPayload
| ResponsePayload
| TextPayload
| AckPayload
| AdvertPayload
| GroupTextPayload
| GroupDataPayload
| AnonReqPayload
| PathPayload
| TracePayload
| RawCustomPayload;
export interface EncryptedPayload {
cipherMAC: Uint8Array;
cipherText: Uint8Array;
}
export interface RequestPayload {
type: PayloadType.REQUEST;
dst: NodeHash;
src: NodeHash;
encrypted: EncryptedPayload;
decrypted?: DecryptedRequest;
}
export enum RequestType {
GET_STATS = 0x01,
KEEP_ALIVE = 0x02,
GET_TELEMETRY = 0x03,
GET_MIN_MAX_AVG = 0x04,
GET_ACL = 0x05,
GET_NEIGHBORS = 0x06,
GET_OWNER_INFO = 0x07,
}
export interface DecryptedRequest {
timestamp: Date;
requestType: RequestType;
requestData: Uint8Array;
}
export interface ResponsePayload {
type: PayloadType.RESPONSE;
dst: NodeHash;
src: NodeHash;
encrypted: EncryptedPayload;
decrypted?: DecryptedResponse;
}
export interface DecryptedResponse {
timestamp: Date;
responseData: Uint8Array;
}
export interface TextPayload {
type: PayloadType.TEXT;
dst: NodeHash;
src: NodeHash;
encrypted: EncryptedPayload;
decrypted?: DecryptedText;
}
export enum TextType {
PLAIN_TEXT = 0x00,
CLI_COMMAND = 0x01,
SIGNED_PLAIN_TEXT = 0x02,
}
export interface DecryptedText {
timestamp: Date;
textType: TextType;
attempt: number;
message: string;
}
export interface AckPayload {
type: PayloadType.ACK;
checksum: Uint8Array;
}
export interface AdvertPayload {
type: PayloadType.ADVERT;
publicKey: Uint8Array;
timestamp: Date;
signature: Uint8Array;
appdata: AdvertAppData;
}
export enum NodeType {
CHAT_NODE = 0x01,
REPEATER = 0x02,
ROOM_SERVER = 0x03,
SENSOR_NODE = 0x04,
}
export interface AdvertAppData {
nodeType: NodeType;
hasLocation: boolean;
location?: [number, number];
hasFeature1: boolean;
feature1?: Uint16;
hasFeature2: boolean;
feature2?: Uint16;
hasName: boolean;
name?: string;
}
export interface GroupTextPayload {
type: PayloadType.GROUP_TEXT;
channelHash: NodeHash;
encrypted: EncryptedPayload;
decrypted?: DecryptedGroupText;
}
export interface DecryptedGroupText {
timestamp: Date;
textType: TextType;
attempt: number;
message: string;
}
export interface GroupDataPayload {
type: PayloadType.GROUP_DATA;
channelHash: NodeHash;
encrypted: EncryptedPayload;
decrypted?: DecryptedGroupData;
}
export interface DecryptedGroupData {
timestamp: Date;
data: Uint8Array;
}
export interface AnonReqPayload {
type: PayloadType.ANON_REQ;
dst: NodeHash;
publicKey: Uint8Array;
encrypted: EncryptedPayload;
decrypted?: DecryptedAnonReq;
}
export interface DecryptedAnonReq {
timestamp: Date;
data: Uint8Array;
}
export interface PathPayload {
type: PayloadType.PATH;
dst: NodeHash;
src: NodeHash;
}
export interface TracePayload {
type: PayloadType.TRACE;
tag: number;
authCode: number;
flags: number;
nodes: Uint8Array;
}
export interface RawCustomPayload {
type: PayloadType.RAW_CUSTOM;
data: Uint8Array;
}
// NodeHash is a hex string of the hash of a node's (partial) public key.
export type NodeHash = string;
/* Contact types and structures */
export interface Group {
name: string;
secret: BaseGroupSecret;
}
export interface Contact {
name: string;
publicKey: Uint8Array;
}
/* Identity and group management. */
export type Secret = Uint8Array | string;
export abstract class BaseIdentity {
publicKey: Uint8Array;
constructor(publicKey: Uint8Array) {
this.publicKey = publicKey;
}
public abstract hash(): NodeHash;
public abstract verify(message: Uint8Array, signature: Uint8Array): boolean;
public matches(other: BaseIdentity | BaseLocalIdentity): boolean {
return equalBytes(this.publicKey, other.publicKey);
}
}
export abstract class BaseLocalIdentity extends BaseIdentity {
privateKey: Uint8Array;
constructor(publicKey: Uint8Array, privateKey: Uint8Array) {
super(publicKey);
this.privateKey = privateKey;
}
public abstract sign(message: Uint8Array): Uint8Array;
public abstract calculateSharedSecret(other: BaseIdentity | Uint8Array): Uint8Array;
}
export abstract class BaseGroup {
name: string;
secret: BaseGroupSecret;
constructor(name: string, secret: BaseGroupSecret) {
this.name = name;
this.secret = secret;
}
decryptText(encrypted: EncryptedPayload): DecryptedGroupText {
return this.secret.decryptText(encrypted);
}
decryptData(encrypted: EncryptedPayload): DecryptedGroupData {
return this.secret.decryptData(encrypted);
}
}
export abstract class BaseGroupSecret {
secret: Uint8Array;
constructor(secret: Secret) {
if (typeof secret === "string") {
secret = hexToBytes(secret);
}
this.secret = secret;
}
public abstract hash(): NodeHash;
public abstract decryptText(encrypted: EncryptedPayload): DecryptedGroupText;
public abstract decryptData(encrypted: EncryptedPayload): DecryptedGroupData;
}
export abstract class BaseKeyManager {
abstract addGroup(group: Group): void;
abstract addGroupSecret(name: string, secret?: Secret): void;
abstract decryptGroupText(channelHash: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedGroupText, group: Group };
abstract decryptGroupData(channelHash: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedGroupData, group: Group };
abstract addLocalIdentity(seed: Secret): void;
abstract addContact(contact: Contact): void;
abstract addIdentity(name: string, publicKey: Uint8Array | string): void;
abstract decryptRequest(dst: NodeHash, src: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedRequest, contact: Contact, identity: BaseIdentity };
abstract decryptResponse(dst: NodeHash, src: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedResponse, contact: Contact, identity: BaseIdentity };
abstract decryptText(dst: NodeHash, src: NodeHash, encrypted: EncryptedPayload): { decrypted: DecryptedText, contact: Contact, identity: BaseIdentity };
abstract decryptAnonReq(dst: NodeHash, publicKey: Uint8Array, encrypted: EncryptedPayload): { decrypted: DecryptedAnonReq, contact: Contact, identity: BaseIdentity };
}