@hamradio/meshcore 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +18 -0
- package/README.md +19 -0
- package/dist/crypto.d.ts +42 -0
- package/dist/crypto.js +199 -0
- package/dist/crypto.types.d.ts +26 -0
- package/dist/crypto.types.js +1 -0
- package/dist/identity.d.ts +65 -0
- package/dist/identity.js +302 -0
- package/dist/identity.types.d.ts +17 -0
- package/dist/identity.types.js +1 -0
- package/dist/index.d.mts +392 -0
- package/dist/index.d.ts +392 -0
- package/dist/index.js +871 -0
- package/dist/index.mjs +834 -0
- package/dist/packet.d.ts +32 -0
- package/dist/packet.js +242 -0
- package/dist/packet.types.d.ts +161 -0
- package/dist/packet.types.js +44 -0
- package/dist/parser.d.ts +31 -0
- package/dist/parser.js +124 -0
- package/package.json +55 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
// src/crypto.ts
|
|
2
|
+
import { ed25519, x25519 } from "@noble/curves/ed25519.js";
|
|
3
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
4
|
+
import { hmac } from "@noble/hashes/hmac.js";
|
|
5
|
+
import { ecb } from "@noble/ciphers/aes.js";
|
|
6
|
+
|
|
7
|
+
// src/parser.ts
|
|
8
|
+
import { equalBytes } from "@noble/ciphers/utils.js";
|
|
9
|
+
import { bytesToHex, hexToBytes as nobleHexToBytes } from "@noble/hashes/utils.js";
|
|
10
|
+
var base64ToBytes = (base64, size) => {
|
|
11
|
+
let normalized = base64.replace(/-/g, "+").replace(/_/g, "/");
|
|
12
|
+
while (normalized.length % 4 !== 0) {
|
|
13
|
+
normalized += "=";
|
|
14
|
+
}
|
|
15
|
+
const binaryString = atob(normalized);
|
|
16
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
17
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
18
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
19
|
+
}
|
|
20
|
+
if (size !== void 0 && bytes.length !== size) {
|
|
21
|
+
throw new Error(`Invalid base64 length: expected ${size} bytes, got ${bytes.length}`);
|
|
22
|
+
}
|
|
23
|
+
return bytes;
|
|
24
|
+
};
|
|
25
|
+
var hexToBytes = (hex, size) => {
|
|
26
|
+
const bytes = nobleHexToBytes(hex);
|
|
27
|
+
if (size !== void 0 && bytes.length !== size) {
|
|
28
|
+
throw new Error(`Invalid hex length: expected ${size} bytes, got ${bytes.length}`);
|
|
29
|
+
}
|
|
30
|
+
return bytes;
|
|
31
|
+
};
|
|
32
|
+
var BufferReader = class {
|
|
33
|
+
constructor(buffer) {
|
|
34
|
+
this.buffer = buffer;
|
|
35
|
+
this.offset = 0;
|
|
36
|
+
}
|
|
37
|
+
readByte() {
|
|
38
|
+
if (!this.hasMore()) throw new Error("read past end");
|
|
39
|
+
return this.buffer[this.offset++];
|
|
40
|
+
}
|
|
41
|
+
readBytes(length) {
|
|
42
|
+
if (length === void 0) {
|
|
43
|
+
length = this.buffer.length - this.offset;
|
|
44
|
+
}
|
|
45
|
+
if (this.remainingBytes() < length) throw new Error("read past end");
|
|
46
|
+
const bytes = this.buffer.slice(this.offset, this.offset + length);
|
|
47
|
+
this.offset += length;
|
|
48
|
+
return bytes;
|
|
49
|
+
}
|
|
50
|
+
hasMore() {
|
|
51
|
+
return this.offset < this.buffer.length;
|
|
52
|
+
}
|
|
53
|
+
remainingBytes() {
|
|
54
|
+
return this.buffer.length - this.offset;
|
|
55
|
+
}
|
|
56
|
+
peekByte() {
|
|
57
|
+
if (!this.hasMore()) throw new Error("read past end");
|
|
58
|
+
return this.buffer[this.offset];
|
|
59
|
+
}
|
|
60
|
+
readUint16LE() {
|
|
61
|
+
if (this.remainingBytes() < 2) throw new Error("read past end");
|
|
62
|
+
const value = this.buffer[this.offset] | this.buffer[this.offset + 1] << 8;
|
|
63
|
+
this.offset += 2;
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
readUint32LE() {
|
|
67
|
+
if (this.remainingBytes() < 4) throw new Error("read past end");
|
|
68
|
+
const value = (this.buffer[this.offset] | this.buffer[this.offset + 1] << 8 | this.buffer[this.offset + 2] << 16 | this.buffer[this.offset + 3] << 24) >>> 0;
|
|
69
|
+
this.offset += 4;
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
readInt16LE() {
|
|
73
|
+
if (this.remainingBytes() < 2) throw new Error("read past end");
|
|
74
|
+
const value = this.buffer[this.offset] | this.buffer[this.offset + 1] << 8;
|
|
75
|
+
this.offset += 2;
|
|
76
|
+
return value < 32768 ? value : value - 65536;
|
|
77
|
+
}
|
|
78
|
+
readInt32LE() {
|
|
79
|
+
if (this.remainingBytes() < 4) throw new Error("read past end");
|
|
80
|
+
const u = (this.buffer[this.offset] | this.buffer[this.offset + 1] << 8 | this.buffer[this.offset + 2] << 16 | this.buffer[this.offset + 3] << 24) >>> 0;
|
|
81
|
+
this.offset += 4;
|
|
82
|
+
return u < 2147483648 ? u : u - 4294967296;
|
|
83
|
+
}
|
|
84
|
+
readTimestamp() {
|
|
85
|
+
const timestamp = this.readUint32LE();
|
|
86
|
+
return new Date(timestamp * 1e3);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
var BufferWriter = class {
|
|
90
|
+
constructor() {
|
|
91
|
+
this.buffer = [];
|
|
92
|
+
}
|
|
93
|
+
writeByte(value) {
|
|
94
|
+
this.buffer.push(value & 255);
|
|
95
|
+
}
|
|
96
|
+
writeBytes(bytes) {
|
|
97
|
+
this.buffer.push(...bytes);
|
|
98
|
+
}
|
|
99
|
+
writeUint16LE(value) {
|
|
100
|
+
this.buffer.push(value & 255, value >> 8 & 255);
|
|
101
|
+
}
|
|
102
|
+
writeUint32LE(value) {
|
|
103
|
+
this.buffer.push(
|
|
104
|
+
value & 255,
|
|
105
|
+
value >> 8 & 255,
|
|
106
|
+
value >> 16 & 255,
|
|
107
|
+
value >> 24 & 255
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
writeInt16LE(value) {
|
|
111
|
+
this.writeUint16LE(value < 0 ? value + 65536 : value);
|
|
112
|
+
}
|
|
113
|
+
writeInt32LE(value) {
|
|
114
|
+
this.writeUint32LE(value < 0 ? value + 4294967296 : value);
|
|
115
|
+
}
|
|
116
|
+
writeTimestamp(date) {
|
|
117
|
+
const timestamp = Math.floor(date.getTime() / 1e3);
|
|
118
|
+
this.writeUint32LE(timestamp);
|
|
119
|
+
}
|
|
120
|
+
toBytes() {
|
|
121
|
+
return new Uint8Array(this.buffer);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// src/crypto.ts
|
|
126
|
+
var PUBLIC_KEY_SIZE = 32;
|
|
127
|
+
var SEED_SIZE = 32;
|
|
128
|
+
var HMAC_SIZE = 2;
|
|
129
|
+
var SHARED_SECRET_SIZE = 32;
|
|
130
|
+
var SIGNATURE_SIZE = 64;
|
|
131
|
+
var STATIC_SECRET_SIZE = 32;
|
|
132
|
+
var publicSecret = hexToBytes("8b3387e9c5cdea6ac9e5edbaa115cd72", 16);
|
|
133
|
+
var PublicKey = class _PublicKey {
|
|
134
|
+
constructor(key) {
|
|
135
|
+
if (typeof key === "string") {
|
|
136
|
+
this.key = hexToBytes(key, PUBLIC_KEY_SIZE);
|
|
137
|
+
} else if (key instanceof Uint8Array) {
|
|
138
|
+
this.key = key;
|
|
139
|
+
} else {
|
|
140
|
+
throw new Error("Invalid type for PublicKey constructor");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
toHash() {
|
|
144
|
+
return sha256.create().update(this.key).digest()[0];
|
|
145
|
+
}
|
|
146
|
+
toBytes() {
|
|
147
|
+
return this.key;
|
|
148
|
+
}
|
|
149
|
+
toString() {
|
|
150
|
+
return bytesToHex(this.key);
|
|
151
|
+
}
|
|
152
|
+
equals(other) {
|
|
153
|
+
let otherKey;
|
|
154
|
+
if (other instanceof _PublicKey) {
|
|
155
|
+
otherKey = other.toBytes();
|
|
156
|
+
} else if (other instanceof Uint8Array) {
|
|
157
|
+
otherKey = other;
|
|
158
|
+
} else if (typeof other === "string") {
|
|
159
|
+
otherKey = hexToBytes(other, PUBLIC_KEY_SIZE);
|
|
160
|
+
} else {
|
|
161
|
+
throw new Error("Invalid type for PublicKey comparison");
|
|
162
|
+
}
|
|
163
|
+
return equalBytes(this.key, otherKey);
|
|
164
|
+
}
|
|
165
|
+
verify(message, signature) {
|
|
166
|
+
if (signature.length !== SIGNATURE_SIZE) {
|
|
167
|
+
throw new Error(`Invalid signature length: expected ${SIGNATURE_SIZE} bytes, got ${signature.length}`);
|
|
168
|
+
}
|
|
169
|
+
return ed25519.verify(signature, message, this.key);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
var PrivateKey = class _PrivateKey {
|
|
173
|
+
constructor(seed) {
|
|
174
|
+
if (typeof seed === "string") {
|
|
175
|
+
seed = hexToBytes(seed, SEED_SIZE);
|
|
176
|
+
}
|
|
177
|
+
if (seed.length !== SEED_SIZE) {
|
|
178
|
+
throw new Error(`Invalid seed length: expected ${SEED_SIZE} bytes, got ${seed.length}`);
|
|
179
|
+
}
|
|
180
|
+
const { secretKey, publicKey } = ed25519.keygen(seed);
|
|
181
|
+
this.secretKey = secretKey;
|
|
182
|
+
this.publicKey = new PublicKey(publicKey);
|
|
183
|
+
}
|
|
184
|
+
toPublicKey() {
|
|
185
|
+
return this.publicKey;
|
|
186
|
+
}
|
|
187
|
+
toBytes() {
|
|
188
|
+
return this.secretKey;
|
|
189
|
+
}
|
|
190
|
+
toString() {
|
|
191
|
+
return bytesToHex(this.secretKey);
|
|
192
|
+
}
|
|
193
|
+
sign(message) {
|
|
194
|
+
return ed25519.sign(message, this.secretKey);
|
|
195
|
+
}
|
|
196
|
+
calculateSharedSecret(other) {
|
|
197
|
+
let otherPublicKey;
|
|
198
|
+
if (other instanceof PublicKey) {
|
|
199
|
+
otherPublicKey = other;
|
|
200
|
+
} else if (other instanceof Uint8Array) {
|
|
201
|
+
otherPublicKey = new PublicKey(other);
|
|
202
|
+
} else if (typeof other === "string") {
|
|
203
|
+
otherPublicKey = new PublicKey(other);
|
|
204
|
+
} else {
|
|
205
|
+
throw new Error("Invalid type for calculateSharedSecret comparison");
|
|
206
|
+
}
|
|
207
|
+
return x25519.getSharedSecret(this.secretKey, otherPublicKey.toBytes());
|
|
208
|
+
}
|
|
209
|
+
static generate() {
|
|
210
|
+
const { secretKey } = ed25519.keygen();
|
|
211
|
+
return new _PrivateKey(secretKey);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
var SharedSecret = class _SharedSecret {
|
|
215
|
+
constructor(secret) {
|
|
216
|
+
if (secret.length === SHARED_SECRET_SIZE / 2) {
|
|
217
|
+
const padded = new Uint8Array(SHARED_SECRET_SIZE);
|
|
218
|
+
padded.set(secret, SHARED_SECRET_SIZE - secret.length);
|
|
219
|
+
secret = padded;
|
|
220
|
+
}
|
|
221
|
+
if (secret.length !== SHARED_SECRET_SIZE) {
|
|
222
|
+
throw new Error(`Invalid shared secret length: expected ${SHARED_SECRET_SIZE} bytes, got ${secret.length}`);
|
|
223
|
+
}
|
|
224
|
+
this.secret = secret;
|
|
225
|
+
}
|
|
226
|
+
toHash() {
|
|
227
|
+
return this.secret[0];
|
|
228
|
+
}
|
|
229
|
+
toBytes() {
|
|
230
|
+
return this.secret;
|
|
231
|
+
}
|
|
232
|
+
toString() {
|
|
233
|
+
return bytesToHex(this.secret);
|
|
234
|
+
}
|
|
235
|
+
decrypt(hmac2, ciphertext) {
|
|
236
|
+
if (hmac2.length !== HMAC_SIZE) {
|
|
237
|
+
throw new Error(`Invalid HMAC length: expected ${HMAC_SIZE} bytes, got ${hmac2.length}`);
|
|
238
|
+
}
|
|
239
|
+
const expectedHmac = this.calculateHmac(ciphertext);
|
|
240
|
+
if (!equalBytes(hmac2, expectedHmac)) {
|
|
241
|
+
throw new Error(`Invalid HMAC: decryption failed: expected ${bytesToHex(expectedHmac)}, got ${bytesToHex(hmac2)}`);
|
|
242
|
+
}
|
|
243
|
+
const cipher = ecb(this.secret.slice(0, 16), { disablePadding: true });
|
|
244
|
+
const plaintext = new Uint8Array(ciphertext.length);
|
|
245
|
+
for (let i = 0; i < ciphertext.length; i += 16) {
|
|
246
|
+
const block = ciphertext.slice(i, i + 16);
|
|
247
|
+
const dec = cipher.decrypt(block);
|
|
248
|
+
plaintext.set(dec, i);
|
|
249
|
+
}
|
|
250
|
+
let end = plaintext.length;
|
|
251
|
+
while (end > 0 && plaintext[end - 1] === 0) {
|
|
252
|
+
end--;
|
|
253
|
+
}
|
|
254
|
+
return plaintext.slice(0, end);
|
|
255
|
+
}
|
|
256
|
+
encrypt(data) {
|
|
257
|
+
const key = this.secret.slice(0, 16);
|
|
258
|
+
const cipher = ecb(key, { disablePadding: true });
|
|
259
|
+
const fullBlocks = Math.floor(data.length / 16);
|
|
260
|
+
const remaining = data.length % 16;
|
|
261
|
+
const ciphertext = new Uint8Array((fullBlocks + (remaining > 0 ? 1 : 0)) * 16);
|
|
262
|
+
for (let i = 0; i < fullBlocks; i++) {
|
|
263
|
+
const block = data.slice(i * 16, (i + 1) * 16);
|
|
264
|
+
const enc = cipher.encrypt(block);
|
|
265
|
+
ciphertext.set(enc, i * 16);
|
|
266
|
+
}
|
|
267
|
+
if (remaining > 0) {
|
|
268
|
+
const lastBlock = new Uint8Array(16);
|
|
269
|
+
lastBlock.set(data.slice(fullBlocks * 16));
|
|
270
|
+
const enc = cipher.encrypt(lastBlock);
|
|
271
|
+
ciphertext.set(enc, fullBlocks * 16);
|
|
272
|
+
}
|
|
273
|
+
const hmac2 = this.calculateHmac(ciphertext);
|
|
274
|
+
return { hmac: hmac2, ciphertext };
|
|
275
|
+
}
|
|
276
|
+
calculateHmac(data) {
|
|
277
|
+
return hmac(sha256, this.secret, data).slice(0, HMAC_SIZE);
|
|
278
|
+
}
|
|
279
|
+
static fromName(name) {
|
|
280
|
+
if (name === "Public") {
|
|
281
|
+
return new _SharedSecret(publicSecret);
|
|
282
|
+
} else if (!/^#/.test(name)) {
|
|
283
|
+
throw new Error("Only the 'Public' group or groups starting with '#' are supported");
|
|
284
|
+
}
|
|
285
|
+
const hash = sha256.create().update(new TextEncoder().encode(name)).digest();
|
|
286
|
+
return new _SharedSecret(hash.slice(0, SHARED_SECRET_SIZE));
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
var StaticSecret = class {
|
|
290
|
+
constructor(secret) {
|
|
291
|
+
if (typeof secret === "string") {
|
|
292
|
+
secret = hexToBytes(secret, STATIC_SECRET_SIZE);
|
|
293
|
+
}
|
|
294
|
+
if (secret.length !== STATIC_SECRET_SIZE) {
|
|
295
|
+
throw new Error(`Invalid static secret length: expected ${STATIC_SECRET_SIZE} bytes, got ${secret.length}`);
|
|
296
|
+
}
|
|
297
|
+
this.secret = secret;
|
|
298
|
+
}
|
|
299
|
+
publicKey() {
|
|
300
|
+
const publicKey = x25519.getPublicKey(this.secret);
|
|
301
|
+
return new PublicKey(publicKey);
|
|
302
|
+
}
|
|
303
|
+
diffieHellman(otherPublicKey) {
|
|
304
|
+
const sharedSecret = x25519.getSharedSecret(this.secret, otherPublicKey.toBytes());
|
|
305
|
+
return new SharedSecret(sharedSecret);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// src/identity.ts
|
|
310
|
+
var parseNodeHash = (hash) => {
|
|
311
|
+
if (hash instanceof Uint8Array) {
|
|
312
|
+
return hash[0];
|
|
313
|
+
}
|
|
314
|
+
if (typeof hash === "number") {
|
|
315
|
+
if (hash < 0 || hash > 255) {
|
|
316
|
+
throw new Error("NodeHash number must be between 0x00 and 0xFF");
|
|
317
|
+
}
|
|
318
|
+
return hash;
|
|
319
|
+
} else if (typeof hash === "string") {
|
|
320
|
+
const parsed = hexToBytes(hash);
|
|
321
|
+
if (parsed.length !== 1) {
|
|
322
|
+
throw new Error("NodeHash string must represent a single byte");
|
|
323
|
+
}
|
|
324
|
+
return parsed[0];
|
|
325
|
+
}
|
|
326
|
+
throw new Error("Invalid NodeHash type");
|
|
327
|
+
};
|
|
328
|
+
var toPublicKeyBytes = (key) => {
|
|
329
|
+
if (key instanceof Identity) {
|
|
330
|
+
return key.publicKey.toBytes();
|
|
331
|
+
} else if (key instanceof PublicKey) {
|
|
332
|
+
return key.toBytes();
|
|
333
|
+
} else if (key instanceof Uint8Array) {
|
|
334
|
+
return key;
|
|
335
|
+
} else if (typeof key === "string") {
|
|
336
|
+
return hexToBytes(key);
|
|
337
|
+
} else {
|
|
338
|
+
throw new Error("Invalid type for toPublicKeyBytes");
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
var Identity = class {
|
|
342
|
+
constructor(publicKey) {
|
|
343
|
+
if (publicKey instanceof PublicKey) {
|
|
344
|
+
this.publicKey = publicKey;
|
|
345
|
+
} else if (publicKey instanceof Uint8Array || typeof publicKey === "string") {
|
|
346
|
+
this.publicKey = new PublicKey(publicKey);
|
|
347
|
+
} else {
|
|
348
|
+
throw new Error("Invalid type for Identity constructor");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
hash() {
|
|
352
|
+
return this.publicKey.toHash();
|
|
353
|
+
}
|
|
354
|
+
toString() {
|
|
355
|
+
return this.publicKey.toString();
|
|
356
|
+
}
|
|
357
|
+
verify(signature, message) {
|
|
358
|
+
return this.publicKey.verify(message, signature);
|
|
359
|
+
}
|
|
360
|
+
matches(other) {
|
|
361
|
+
return this.publicKey.equals(toPublicKeyBytes(other));
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
var LocalIdentity = class extends Identity {
|
|
365
|
+
constructor(privateKey, publicKey) {
|
|
366
|
+
if (publicKey instanceof PublicKey) {
|
|
367
|
+
super(publicKey.toBytes());
|
|
368
|
+
} else {
|
|
369
|
+
super(publicKey);
|
|
370
|
+
}
|
|
371
|
+
if (privateKey instanceof PrivateKey) {
|
|
372
|
+
this.privateKey = privateKey;
|
|
373
|
+
} else {
|
|
374
|
+
this.privateKey = new PrivateKey(privateKey);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
sign(message) {
|
|
378
|
+
return this.privateKey.sign(message);
|
|
379
|
+
}
|
|
380
|
+
calculateSharedSecret(other) {
|
|
381
|
+
let otherPublicKey;
|
|
382
|
+
if (other instanceof Identity) {
|
|
383
|
+
otherPublicKey = other.publicKey;
|
|
384
|
+
} else if ("toBytes" in other) {
|
|
385
|
+
otherPublicKey = new PublicKey(other.toBytes());
|
|
386
|
+
} else if ("publicKey" in other && other.publicKey instanceof Uint8Array) {
|
|
387
|
+
otherPublicKey = new PublicKey(other.publicKey);
|
|
388
|
+
} else if ("publicKey" in other && other.publicKey instanceof PublicKey) {
|
|
389
|
+
otherPublicKey = other.publicKey;
|
|
390
|
+
} else if ("publicKey" in other && typeof other.publicKey === "function") {
|
|
391
|
+
otherPublicKey = new PublicKey(other.publicKey().toBytes());
|
|
392
|
+
} else {
|
|
393
|
+
throw new Error("Invalid type for calculateSharedSecret comparison");
|
|
394
|
+
}
|
|
395
|
+
return new SharedSecret(this.privateKey.calculateSharedSecret(otherPublicKey));
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
var Contact = class {
|
|
399
|
+
constructor(name, identity) {
|
|
400
|
+
this.name = "";
|
|
401
|
+
this.name = name;
|
|
402
|
+
if (identity instanceof Identity) {
|
|
403
|
+
this.identity = identity;
|
|
404
|
+
} else if (identity instanceof PublicKey) {
|
|
405
|
+
this.identity = new Identity(identity);
|
|
406
|
+
} else if (identity instanceof Uint8Array || typeof identity === "string") {
|
|
407
|
+
this.identity = new Identity(identity);
|
|
408
|
+
} else {
|
|
409
|
+
throw new Error("Invalid type for Contact constructor");
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
matches(hash) {
|
|
413
|
+
return this.identity.publicKey.equals(hash);
|
|
414
|
+
}
|
|
415
|
+
publicKey() {
|
|
416
|
+
return this.identity.publicKey;
|
|
417
|
+
}
|
|
418
|
+
calculateSharedSecret(me) {
|
|
419
|
+
return me.calculateSharedSecret(this.identity);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
var Group = class {
|
|
423
|
+
constructor(name, secret) {
|
|
424
|
+
this.name = name;
|
|
425
|
+
if (secret) {
|
|
426
|
+
this.secret = secret;
|
|
427
|
+
} else {
|
|
428
|
+
this.secret = SharedSecret.fromName(name);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
hash() {
|
|
432
|
+
return this.secret.toHash();
|
|
433
|
+
}
|
|
434
|
+
decryptText(hmac2, ciphertext) {
|
|
435
|
+
const data = this.secret.decrypt(hmac2, ciphertext);
|
|
436
|
+
if (data.length < 5) {
|
|
437
|
+
throw new Error("Invalid ciphertext");
|
|
438
|
+
}
|
|
439
|
+
const reader = new BufferReader(data);
|
|
440
|
+
const timestamp = reader.readTimestamp();
|
|
441
|
+
const flags = reader.readByte();
|
|
442
|
+
const textType = flags >> 2 & 63;
|
|
443
|
+
const attempt = flags & 3;
|
|
444
|
+
const message = new TextDecoder("utf-8").decode(reader.readBytes());
|
|
445
|
+
return {
|
|
446
|
+
timestamp,
|
|
447
|
+
textType,
|
|
448
|
+
attempt,
|
|
449
|
+
message
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
encryptText(plain) {
|
|
453
|
+
const writer = new BufferWriter();
|
|
454
|
+
writer.writeTimestamp(plain.timestamp);
|
|
455
|
+
const flags = (plain.textType & 63) << 2 | plain.attempt & 3;
|
|
456
|
+
writer.writeByte(flags);
|
|
457
|
+
writer.writeBytes(new TextEncoder().encode(plain.message));
|
|
458
|
+
const data = writer.toBytes();
|
|
459
|
+
return this.secret.encrypt(data);
|
|
460
|
+
}
|
|
461
|
+
decryptData(hmac2, ciphertext) {
|
|
462
|
+
const data = this.secret.decrypt(hmac2, ciphertext);
|
|
463
|
+
if (data.length < 4) {
|
|
464
|
+
throw new Error("Invalid ciphertext");
|
|
465
|
+
}
|
|
466
|
+
const reader = new BufferReader(data);
|
|
467
|
+
return {
|
|
468
|
+
timestamp: reader.readTimestamp(),
|
|
469
|
+
data: reader.readBytes(reader.remainingBytes())
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
encryptData(plain) {
|
|
473
|
+
const writer = new BufferWriter();
|
|
474
|
+
writer.writeTimestamp(plain.timestamp);
|
|
475
|
+
writer.writeBytes(plain.data);
|
|
476
|
+
const data = writer.toBytes();
|
|
477
|
+
return this.secret.encrypt(data);
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
var Contacts = class {
|
|
481
|
+
constructor() {
|
|
482
|
+
this.localIdentities = [];
|
|
483
|
+
this.contacts = {};
|
|
484
|
+
this.groups = {};
|
|
485
|
+
}
|
|
486
|
+
addLocalIdentity(identity) {
|
|
487
|
+
this.localIdentities.push({ identity, sharedSecrets: {} });
|
|
488
|
+
}
|
|
489
|
+
addContact(contact) {
|
|
490
|
+
const hash = parseNodeHash(contact.identity.hash());
|
|
491
|
+
if (!this.contacts[hash]) {
|
|
492
|
+
this.contacts[hash] = [];
|
|
493
|
+
}
|
|
494
|
+
this.contacts[hash].push(contact);
|
|
495
|
+
}
|
|
496
|
+
decrypt(src, dst, hmac2, ciphertext) {
|
|
497
|
+
let contacts = [];
|
|
498
|
+
if (src instanceof PublicKey) {
|
|
499
|
+
const srcHash = parseNodeHash(src.toHash());
|
|
500
|
+
for (const contact of this.contacts[srcHash] || []) {
|
|
501
|
+
if (contact.identity.matches(src)) {
|
|
502
|
+
contacts.push(contact);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (contacts.length === 0) {
|
|
506
|
+
contacts.push(new Contact("", new Identity(src.toBytes())));
|
|
507
|
+
}
|
|
508
|
+
} else {
|
|
509
|
+
const srcHash = parseNodeHash(src);
|
|
510
|
+
contacts = this.contacts[srcHash] || [];
|
|
511
|
+
}
|
|
512
|
+
if (contacts.length === 0) {
|
|
513
|
+
throw new Error("Unknown source hash");
|
|
514
|
+
}
|
|
515
|
+
const dstHash = parseNodeHash(dst);
|
|
516
|
+
const localIdentities = this.localIdentities.filter((li) => li.identity.publicKey.key[0] === dstHash);
|
|
517
|
+
if (localIdentities.length === 0) {
|
|
518
|
+
throw new Error("Unknown destination hash");
|
|
519
|
+
}
|
|
520
|
+
for (const localIdentity of localIdentities) {
|
|
521
|
+
for (const contact of contacts) {
|
|
522
|
+
const sharedSecret = this.calculateSharedSecret(localIdentity, contact);
|
|
523
|
+
try {
|
|
524
|
+
const decrypted = sharedSecret.decrypt(hmac2, ciphertext);
|
|
525
|
+
return { localIdentity: localIdentity.identity, contact, decrypted };
|
|
526
|
+
} catch {
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
throw new Error("Decryption failed with all known identities and contacts");
|
|
531
|
+
}
|
|
532
|
+
// Caches the calculated shared secret for a given local identity and contact to avoid redundant calculations.
|
|
533
|
+
calculateSharedSecret(localIdentity, contact) {
|
|
534
|
+
const cacheKey = contact.identity.toString();
|
|
535
|
+
if (localIdentity.sharedSecrets[cacheKey]) {
|
|
536
|
+
return localIdentity.sharedSecrets[cacheKey];
|
|
537
|
+
}
|
|
538
|
+
const sharedSecret = localIdentity.identity.calculateSharedSecret(contact.identity);
|
|
539
|
+
localIdentity.sharedSecrets[cacheKey] = sharedSecret;
|
|
540
|
+
return sharedSecret;
|
|
541
|
+
}
|
|
542
|
+
addGroup(group) {
|
|
543
|
+
const hash = parseNodeHash(group.hash());
|
|
544
|
+
if (!this.groups[hash]) {
|
|
545
|
+
this.groups[hash] = [];
|
|
546
|
+
}
|
|
547
|
+
this.groups[hash].push(group);
|
|
548
|
+
}
|
|
549
|
+
decryptGroupText(channelHash, hmac2, ciphertext) {
|
|
550
|
+
const hash = parseNodeHash(channelHash);
|
|
551
|
+
const groups = this.groups[hash] || [];
|
|
552
|
+
if (groups.length === 0) {
|
|
553
|
+
throw new Error("Unknown group hash");
|
|
554
|
+
}
|
|
555
|
+
for (const group of groups) {
|
|
556
|
+
try {
|
|
557
|
+
const decrypted = group.decryptText(hmac2, ciphertext);
|
|
558
|
+
return { decrypted, group };
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
throw new Error("Decryption failed with all known groups");
|
|
563
|
+
}
|
|
564
|
+
decryptGroupData(channelHash, hmac2, ciphertext) {
|
|
565
|
+
const hash = parseNodeHash(channelHash);
|
|
566
|
+
const groups = this.groups[hash] || [];
|
|
567
|
+
if (groups.length === 0) {
|
|
568
|
+
throw new Error("Unknown group hash");
|
|
569
|
+
}
|
|
570
|
+
for (const group of groups) {
|
|
571
|
+
try {
|
|
572
|
+
const decrypted = group.decryptData(hmac2, ciphertext);
|
|
573
|
+
return { decrypted, group };
|
|
574
|
+
} catch {
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
throw new Error("Decryption failed with all known groups");
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// src/packet.ts
|
|
582
|
+
import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
|
|
583
|
+
var Packet = class _Packet {
|
|
584
|
+
constructor(header, transport, pathLength, path, payload) {
|
|
585
|
+
this.header = header;
|
|
586
|
+
this.transport = transport;
|
|
587
|
+
this.pathLength = pathLength;
|
|
588
|
+
this.path = path;
|
|
589
|
+
this.payload = payload;
|
|
590
|
+
this.routeType = header & 3;
|
|
591
|
+
this.payloadVersion = header >> 6 & 3;
|
|
592
|
+
this.payloadType = header >> 2 & 15;
|
|
593
|
+
this.pathHashCount = (pathLength >> 6) + 1;
|
|
594
|
+
this.pathHashSize = pathLength & 63;
|
|
595
|
+
this.pathHashBytes = this.pathHashCount * this.pathHashSize;
|
|
596
|
+
this.pathHashes = [];
|
|
597
|
+
for (let i = 0; i < this.pathHashCount; i++) {
|
|
598
|
+
const hashBytes = this.path.slice(i * this.pathHashSize, (i + 1) * this.pathHashSize);
|
|
599
|
+
const hashHex = bytesToHex(hashBytes);
|
|
600
|
+
this.pathHashes.push(hashHex);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
static fromBytes(bytes) {
|
|
604
|
+
if (typeof bytes === "string") {
|
|
605
|
+
bytes = base64ToBytes(bytes);
|
|
606
|
+
}
|
|
607
|
+
let offset = 0;
|
|
608
|
+
const header = bytes[offset++];
|
|
609
|
+
const routeType = header & 3;
|
|
610
|
+
let transport;
|
|
611
|
+
if (_Packet.hasTransportCodes(routeType)) {
|
|
612
|
+
const uitn16View = new DataView(bytes.buffer, bytes.byteOffset + offset, 4);
|
|
613
|
+
transport = [uitn16View.getUint16(0, false), uitn16View.getUint16(2, false)];
|
|
614
|
+
offset += 4;
|
|
615
|
+
}
|
|
616
|
+
const pathLength = bytes[offset++];
|
|
617
|
+
const path = bytes.slice(offset, offset + pathLength);
|
|
618
|
+
offset += pathLength;
|
|
619
|
+
const payload = bytes.slice(offset);
|
|
620
|
+
return new _Packet(header, transport, pathLength, path, payload);
|
|
621
|
+
}
|
|
622
|
+
static hasTransportCodes(routeType) {
|
|
623
|
+
return routeType === 0 /* TRANSPORT_FLOOD */ || routeType === 3 /* TRANSPORT_DIRECT */;
|
|
624
|
+
}
|
|
625
|
+
hash() {
|
|
626
|
+
const hash = sha2562.create();
|
|
627
|
+
hash.update(new Uint8Array([this.payloadType]));
|
|
628
|
+
if (this.payloadType === 9 /* TRACE */) {
|
|
629
|
+
hash.update(new Uint8Array([this.pathLength]));
|
|
630
|
+
}
|
|
631
|
+
hash.update(this.payload);
|
|
632
|
+
const digest = hash.digest();
|
|
633
|
+
return bytesToHex(digest.slice(0, 8));
|
|
634
|
+
}
|
|
635
|
+
decode() {
|
|
636
|
+
switch (this.payloadType) {
|
|
637
|
+
case 0 /* REQUEST */:
|
|
638
|
+
return this.decodeRequest();
|
|
639
|
+
case 1 /* RESPONSE */:
|
|
640
|
+
return this.decodeResponse();
|
|
641
|
+
case 2 /* TEXT */:
|
|
642
|
+
return this.decodeText();
|
|
643
|
+
case 3 /* ACK */:
|
|
644
|
+
return this.decodeAck();
|
|
645
|
+
case 4 /* ADVERT */:
|
|
646
|
+
return this.decodeAdvert();
|
|
647
|
+
case 5 /* GROUP_TEXT */:
|
|
648
|
+
return this.decodeGroupText();
|
|
649
|
+
case 6 /* GROUP_DATA */:
|
|
650
|
+
return this.decodeGroupData();
|
|
651
|
+
case 7 /* ANON_REQ */:
|
|
652
|
+
return this.decodeAnonReq();
|
|
653
|
+
case 8 /* PATH */:
|
|
654
|
+
return this.decodePath();
|
|
655
|
+
case 9 /* TRACE */:
|
|
656
|
+
return this.decodeTrace();
|
|
657
|
+
case 15 /* RAW_CUSTOM */:
|
|
658
|
+
return this.decodeRawCustom();
|
|
659
|
+
default:
|
|
660
|
+
throw new Error(`Unsupported payload type: ${this.payloadType}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
decodeEncryptedPayload(reader) {
|
|
664
|
+
const cipherMAC = reader.readBytes(2);
|
|
665
|
+
const cipherText = reader.readBytes(reader.remainingBytes());
|
|
666
|
+
return { cipherMAC, cipherText };
|
|
667
|
+
}
|
|
668
|
+
decodeRequest() {
|
|
669
|
+
if (this.payload.length < 4) {
|
|
670
|
+
throw new Error("Invalid request payload: too short");
|
|
671
|
+
}
|
|
672
|
+
const reader = new BufferReader(this.payload);
|
|
673
|
+
return {
|
|
674
|
+
type: 0 /* REQUEST */,
|
|
675
|
+
dst: reader.readByte(),
|
|
676
|
+
src: reader.readByte(),
|
|
677
|
+
encrypted: this.decodeEncryptedPayload(reader)
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
decodeResponse() {
|
|
681
|
+
if (this.payload.length < 4) {
|
|
682
|
+
throw new Error("Invalid response payload: too short");
|
|
683
|
+
}
|
|
684
|
+
const reader = new BufferReader(this.payload);
|
|
685
|
+
return {
|
|
686
|
+
type: 1 /* RESPONSE */,
|
|
687
|
+
dst: reader.readByte(),
|
|
688
|
+
src: reader.readByte(),
|
|
689
|
+
encrypted: this.decodeEncryptedPayload(reader)
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
decodeText() {
|
|
693
|
+
if (this.payload.length < 4) {
|
|
694
|
+
throw new Error("Invalid text payload: too short");
|
|
695
|
+
}
|
|
696
|
+
const reader = new BufferReader(this.payload);
|
|
697
|
+
return {
|
|
698
|
+
type: 2 /* TEXT */,
|
|
699
|
+
dst: reader.readByte(),
|
|
700
|
+
src: reader.readByte(),
|
|
701
|
+
encrypted: this.decodeEncryptedPayload(reader)
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
decodeAck() {
|
|
705
|
+
if (this.payload.length < 4) {
|
|
706
|
+
throw new Error("Invalid ack payload: too short");
|
|
707
|
+
}
|
|
708
|
+
const reader = new BufferReader(this.payload);
|
|
709
|
+
return {
|
|
710
|
+
type: 3 /* ACK */,
|
|
711
|
+
checksum: reader.readBytes(4)
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
decodeAdvert() {
|
|
715
|
+
if (this.payload.length < 4) {
|
|
716
|
+
throw new Error("Invalid advert payload: too short");
|
|
717
|
+
}
|
|
718
|
+
const reader = new BufferReader(this.payload);
|
|
719
|
+
const payload = {
|
|
720
|
+
type: 4 /* ADVERT */,
|
|
721
|
+
publicKey: reader.readBytes(32),
|
|
722
|
+
timestamp: reader.readTimestamp(),
|
|
723
|
+
signature: reader.readBytes(64)
|
|
724
|
+
};
|
|
725
|
+
const flags = reader.readByte();
|
|
726
|
+
const appdata = {
|
|
727
|
+
nodeType: flags & 15,
|
|
728
|
+
hasLocation: (flags & 16) !== 0,
|
|
729
|
+
hasFeature1: (flags & 32) !== 0,
|
|
730
|
+
hasFeature2: (flags & 64) !== 0,
|
|
731
|
+
hasName: (flags & 128) !== 0
|
|
732
|
+
};
|
|
733
|
+
if (appdata.hasLocation) {
|
|
734
|
+
const lat = reader.readInt32LE() / 1e5;
|
|
735
|
+
const lon = reader.readInt32LE() / 1e5;
|
|
736
|
+
appdata.location = [lat, lon];
|
|
737
|
+
}
|
|
738
|
+
if (appdata.hasFeature1) {
|
|
739
|
+
appdata.feature1 = reader.readUint16LE();
|
|
740
|
+
}
|
|
741
|
+
if (appdata.hasFeature2) {
|
|
742
|
+
appdata.feature2 = reader.readUint16LE();
|
|
743
|
+
}
|
|
744
|
+
if (appdata.hasName) {
|
|
745
|
+
const nameBytes = reader.readBytes();
|
|
746
|
+
let nullPos = nameBytes.indexOf(0);
|
|
747
|
+
if (nullPos === -1) {
|
|
748
|
+
nullPos = nameBytes.length;
|
|
749
|
+
}
|
|
750
|
+
appdata.name = new TextDecoder("utf-8").decode(nameBytes.subarray(0, nullPos));
|
|
751
|
+
}
|
|
752
|
+
return {
|
|
753
|
+
...payload,
|
|
754
|
+
appdata
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
decodeGroupText() {
|
|
758
|
+
if (this.payload.length < 3) {
|
|
759
|
+
throw new Error("Invalid group text payload: too short");
|
|
760
|
+
}
|
|
761
|
+
const reader = new BufferReader(this.payload);
|
|
762
|
+
return {
|
|
763
|
+
type: 5 /* GROUP_TEXT */,
|
|
764
|
+
channelHash: reader.readByte(),
|
|
765
|
+
encrypted: this.decodeEncryptedPayload(reader)
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
decodeGroupData() {
|
|
769
|
+
if (this.payload.length < 3) {
|
|
770
|
+
throw new Error("Invalid group data payload: too short");
|
|
771
|
+
}
|
|
772
|
+
const reader = new BufferReader(this.payload);
|
|
773
|
+
return {
|
|
774
|
+
type: 6 /* GROUP_DATA */,
|
|
775
|
+
channelHash: reader.readByte(),
|
|
776
|
+
encrypted: this.decodeEncryptedPayload(reader)
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
decodeAnonReq() {
|
|
780
|
+
if (this.payload.length < 1 + 32 + 2) {
|
|
781
|
+
throw new Error("Invalid anon req payload: too short");
|
|
782
|
+
}
|
|
783
|
+
const reader = new BufferReader(this.payload);
|
|
784
|
+
return {
|
|
785
|
+
type: 7 /* ANON_REQ */,
|
|
786
|
+
dst: reader.readByte(),
|
|
787
|
+
publicKey: reader.readBytes(32),
|
|
788
|
+
encrypted: this.decodeEncryptedPayload(reader)
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
decodePath() {
|
|
792
|
+
if (this.payload.length < 2) {
|
|
793
|
+
throw new Error("Invalid path payload: too short");
|
|
794
|
+
}
|
|
795
|
+
const reader = new BufferReader(this.payload);
|
|
796
|
+
return {
|
|
797
|
+
type: 8 /* PATH */,
|
|
798
|
+
dst: reader.readByte(),
|
|
799
|
+
src: reader.readByte()
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
decodeTrace() {
|
|
803
|
+
if (this.payload.length < 9) {
|
|
804
|
+
throw new Error("Invalid trace payload: too short");
|
|
805
|
+
}
|
|
806
|
+
const reader = new BufferReader(this.payload);
|
|
807
|
+
return {
|
|
808
|
+
type: 9 /* TRACE */,
|
|
809
|
+
tag: reader.readUint32LE() >>> 0,
|
|
810
|
+
authCode: reader.readUint32LE() >>> 0,
|
|
811
|
+
flags: reader.readByte() & 3,
|
|
812
|
+
nodes: reader.readBytes()
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
decodeRawCustom() {
|
|
816
|
+
return {
|
|
817
|
+
type: 15 /* RAW_CUSTOM */,
|
|
818
|
+
data: this.payload
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
export {
|
|
823
|
+
Contact,
|
|
824
|
+
Contacts,
|
|
825
|
+
Group,
|
|
826
|
+
Identity,
|
|
827
|
+
LocalIdentity,
|
|
828
|
+
Packet,
|
|
829
|
+
PrivateKey,
|
|
830
|
+
PublicKey,
|
|
831
|
+
SharedSecret,
|
|
832
|
+
StaticSecret,
|
|
833
|
+
parseNodeHash
|
|
834
|
+
};
|