@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 ADDED
@@ -0,0 +1,18 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ham
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6
+ associated documentation files (the "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or substantial
12
+ portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
15
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
18
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # meshcore
2
+
3
+ TypeScript library for MeshCore protocol utilities.
4
+
5
+ Quick start
6
+
7
+ 1. Install dev dependencies:
8
+
9
+ ```bash
10
+ npm install --save-dev typescript tsup
11
+ ```
12
+
13
+ 2. Build the library:
14
+
15
+ ```bash
16
+ npm run build
17
+ ```
18
+
19
+ 3. Use the build output from the `dist/` folder or publish to npm.
@@ -0,0 +1,42 @@
1
+ import { IPublicKey, ISharedSecret, IStaticSecret } from './crypto.types';
2
+ import { NodeHash } from './identity.types';
3
+ export declare class PublicKey implements IPublicKey {
4
+ key: Uint8Array;
5
+ constructor(key: Uint8Array | string);
6
+ toHash(): NodeHash;
7
+ toBytes(): Uint8Array;
8
+ toString(): string;
9
+ equals(other: PublicKey | Uint8Array | string): boolean;
10
+ verify(message: Uint8Array, signature: Uint8Array): boolean;
11
+ }
12
+ export declare class PrivateKey {
13
+ private secretKey;
14
+ private publicKey;
15
+ constructor(seed: Uint8Array | string);
16
+ toPublicKey(): PublicKey;
17
+ toBytes(): Uint8Array;
18
+ toString(): string;
19
+ sign(message: Uint8Array): Uint8Array;
20
+ calculateSharedSecret(other: PublicKey | Uint8Array | string): Uint8Array;
21
+ static generate(): PrivateKey;
22
+ }
23
+ export declare class SharedSecret implements ISharedSecret {
24
+ private secret;
25
+ constructor(secret: Uint8Array);
26
+ toHash(): NodeHash;
27
+ toBytes(): Uint8Array;
28
+ toString(): string;
29
+ decrypt(hmac: Uint8Array, ciphertext: Uint8Array): Uint8Array;
30
+ encrypt(data: Uint8Array): {
31
+ hmac: Uint8Array;
32
+ ciphertext: Uint8Array;
33
+ };
34
+ private calculateHmac;
35
+ static fromName(name: string): SharedSecret;
36
+ }
37
+ export declare class StaticSecret implements IStaticSecret {
38
+ private secret;
39
+ constructor(secret: Uint8Array | string);
40
+ publicKey(): IPublicKey;
41
+ diffieHellman(otherPublicKey: IPublicKey): SharedSecret;
42
+ }
package/dist/crypto.js ADDED
@@ -0,0 +1,199 @@
1
+ import { ed25519, x25519 } from '@noble/curves/ed25519.js';
2
+ import { sha256 } from "@noble/hashes/sha2.js";
3
+ import { hmac } from '@noble/hashes/hmac.js';
4
+ import { ecb } from '@noble/ciphers/aes.js';
5
+ import { bytesToHex, equalBytes, hexToBytes } from "./parser";
6
+ const PUBLIC_KEY_SIZE = 32;
7
+ const SEED_SIZE = 32;
8
+ const HMAC_SIZE = 2;
9
+ const SHARED_SECRET_SIZE = 32;
10
+ const SIGNATURE_SIZE = 64;
11
+ const STATIC_SECRET_SIZE = 32;
12
+ // The "Public" group is a special group that all nodes are implicitly part of.
13
+ const publicSecret = hexToBytes("8b3387e9c5cdea6ac9e5edbaa115cd72", 16);
14
+ export class PublicKey {
15
+ constructor(key) {
16
+ if (typeof key === 'string') {
17
+ this.key = hexToBytes(key, PUBLIC_KEY_SIZE);
18
+ }
19
+ else if (key instanceof Uint8Array) {
20
+ this.key = key;
21
+ }
22
+ else {
23
+ throw new Error('Invalid type for PublicKey constructor');
24
+ }
25
+ }
26
+ toHash() {
27
+ return sha256.create().update(this.key).digest()[0];
28
+ }
29
+ toBytes() {
30
+ return this.key;
31
+ }
32
+ toString() {
33
+ return bytesToHex(this.key);
34
+ }
35
+ equals(other) {
36
+ let otherKey;
37
+ if (other instanceof PublicKey) {
38
+ otherKey = other.toBytes();
39
+ }
40
+ else if (other instanceof Uint8Array) {
41
+ otherKey = other;
42
+ }
43
+ else if (typeof other === 'string') {
44
+ otherKey = hexToBytes(other, PUBLIC_KEY_SIZE);
45
+ }
46
+ else {
47
+ throw new Error('Invalid type for PublicKey comparison');
48
+ }
49
+ return equalBytes(this.key, otherKey);
50
+ }
51
+ verify(message, signature) {
52
+ if (signature.length !== SIGNATURE_SIZE) {
53
+ throw new Error(`Invalid signature length: expected ${SIGNATURE_SIZE} bytes, got ${signature.length}`);
54
+ }
55
+ return ed25519.verify(signature, message, this.key);
56
+ }
57
+ }
58
+ export class PrivateKey {
59
+ constructor(seed) {
60
+ if (typeof seed === 'string') {
61
+ seed = hexToBytes(seed, SEED_SIZE);
62
+ }
63
+ if (seed.length !== SEED_SIZE) {
64
+ throw new Error(`Invalid seed length: expected ${SEED_SIZE} bytes, got ${seed.length}`);
65
+ }
66
+ const { secretKey, publicKey } = ed25519.keygen(seed); // Validate seed by generating keys
67
+ this.secretKey = secretKey;
68
+ this.publicKey = new PublicKey(publicKey);
69
+ }
70
+ toPublicKey() {
71
+ return this.publicKey;
72
+ }
73
+ toBytes() {
74
+ return this.secretKey;
75
+ }
76
+ toString() {
77
+ return bytesToHex(this.secretKey);
78
+ }
79
+ sign(message) {
80
+ return ed25519.sign(message, this.secretKey);
81
+ }
82
+ calculateSharedSecret(other) {
83
+ let otherPublicKey;
84
+ if (other instanceof PublicKey) {
85
+ otherPublicKey = other;
86
+ }
87
+ else if (other instanceof Uint8Array) {
88
+ otherPublicKey = new PublicKey(other);
89
+ }
90
+ else if (typeof other === 'string') {
91
+ otherPublicKey = new PublicKey(other);
92
+ }
93
+ else {
94
+ throw new Error('Invalid type for calculateSharedSecret comparison');
95
+ }
96
+ return x25519.getSharedSecret(this.secretKey, otherPublicKey.toBytes());
97
+ }
98
+ static generate() {
99
+ const { secretKey } = ed25519.keygen(); // Ensure ed25519 is initialized
100
+ return new PrivateKey(secretKey);
101
+ }
102
+ }
103
+ export class SharedSecret {
104
+ constructor(secret) {
105
+ if (secret.length === SHARED_SECRET_SIZE / 2) {
106
+ // Zero pad to the left if the secret is too short (e.g. from x25519)
107
+ const padded = new Uint8Array(SHARED_SECRET_SIZE);
108
+ padded.set(secret, SHARED_SECRET_SIZE - secret.length);
109
+ secret = padded;
110
+ }
111
+ if (secret.length !== SHARED_SECRET_SIZE) {
112
+ throw new Error(`Invalid shared secret length: expected ${SHARED_SECRET_SIZE} bytes, got ${secret.length}`);
113
+ }
114
+ this.secret = secret;
115
+ }
116
+ toHash() {
117
+ return this.secret[0];
118
+ }
119
+ toBytes() {
120
+ return this.secret;
121
+ }
122
+ toString() {
123
+ return bytesToHex(this.secret);
124
+ }
125
+ decrypt(hmac, ciphertext) {
126
+ if (hmac.length !== HMAC_SIZE) {
127
+ throw new Error(`Invalid HMAC length: expected ${HMAC_SIZE} bytes, got ${hmac.length}`);
128
+ }
129
+ const expectedHmac = this.calculateHmac(ciphertext);
130
+ if (!equalBytes(hmac, expectedHmac)) {
131
+ throw new Error(`Invalid HMAC: decryption failed: expected ${bytesToHex(expectedHmac)}, got ${bytesToHex(hmac)}`);
132
+ }
133
+ const cipher = ecb(this.secret.slice(0, 16), { disablePadding: true });
134
+ const plaintext = new Uint8Array(ciphertext.length);
135
+ for (let i = 0; i < ciphertext.length; i += 16) {
136
+ const block = ciphertext.slice(i, i + 16);
137
+ const dec = cipher.decrypt(block);
138
+ plaintext.set(dec, i);
139
+ }
140
+ // Remove trailing null bytes (0x00) due to padding
141
+ let end = plaintext.length;
142
+ while (end > 0 && plaintext[end - 1] === 0) {
143
+ end--;
144
+ }
145
+ return plaintext.slice(0, end);
146
+ }
147
+ encrypt(data) {
148
+ const key = this.secret.slice(0, 16);
149
+ const cipher = ecb(key, { disablePadding: true });
150
+ const fullBlocks = Math.floor(data.length / 16);
151
+ const remaining = data.length % 16;
152
+ const ciphertext = new Uint8Array((fullBlocks + (remaining > 0 ? 1 : 0)) * 16);
153
+ for (let i = 0; i < fullBlocks; i++) {
154
+ const block = data.slice(i * 16, (i + 1) * 16);
155
+ const enc = cipher.encrypt(block);
156
+ ciphertext.set(enc, i * 16);
157
+ }
158
+ if (remaining > 0) {
159
+ const lastBlock = new Uint8Array(16);
160
+ lastBlock.set(data.slice(fullBlocks * 16));
161
+ const enc = cipher.encrypt(lastBlock);
162
+ ciphertext.set(enc, fullBlocks * 16);
163
+ }
164
+ const hmac = this.calculateHmac(ciphertext);
165
+ return { hmac, ciphertext };
166
+ }
167
+ calculateHmac(data) {
168
+ return hmac(sha256, this.secret, data).slice(0, HMAC_SIZE);
169
+ }
170
+ static fromName(name) {
171
+ if (name === "Public") {
172
+ return new SharedSecret(publicSecret);
173
+ }
174
+ else if (!/^#/.test(name)) {
175
+ throw new Error("Only the 'Public' group or groups starting with '#' are supported");
176
+ }
177
+ const hash = sha256.create().update(new TextEncoder().encode(name)).digest();
178
+ return new SharedSecret(hash.slice(0, SHARED_SECRET_SIZE));
179
+ }
180
+ }
181
+ export class StaticSecret {
182
+ constructor(secret) {
183
+ if (typeof secret === 'string') {
184
+ secret = hexToBytes(secret, STATIC_SECRET_SIZE);
185
+ }
186
+ if (secret.length !== STATIC_SECRET_SIZE) {
187
+ throw new Error(`Invalid static secret length: expected ${STATIC_SECRET_SIZE} bytes, got ${secret.length}`);
188
+ }
189
+ this.secret = secret;
190
+ }
191
+ publicKey() {
192
+ const publicKey = x25519.getPublicKey(this.secret);
193
+ return new PublicKey(publicKey);
194
+ }
195
+ diffieHellman(otherPublicKey) {
196
+ const sharedSecret = x25519.getSharedSecret(this.secret, otherPublicKey.toBytes());
197
+ return new SharedSecret(sharedSecret);
198
+ }
199
+ }
@@ -0,0 +1,26 @@
1
+ import { NodeHash } from "./identity.types";
2
+ export interface IPublicKey {
3
+ toHash(): NodeHash;
4
+ toBytes(): Uint8Array;
5
+ toString(): string;
6
+ equals(other: IPublicKey | Uint8Array | string): boolean;
7
+ verify(message: Uint8Array, signature: Uint8Array): boolean;
8
+ }
9
+ export interface IPrivateKey extends IPublicKey {
10
+ toPublicKey(): IPublicKey;
11
+ sign(message: Uint8Array): Uint8Array;
12
+ }
13
+ export interface ISharedSecret {
14
+ toHash(): NodeHash;
15
+ toBytes(): Uint8Array;
16
+ toString(): string;
17
+ decrypt(hmac: Uint8Array, ciphertext: Uint8Array): Uint8Array;
18
+ encrypt(data: Uint8Array): {
19
+ hmac: Uint8Array;
20
+ ciphertext: Uint8Array;
21
+ };
22
+ }
23
+ export interface IStaticSecret {
24
+ publicKey(): IPublicKey;
25
+ diffieHellman(otherPublicKey: IPublicKey): ISharedSecret;
26
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,65 @@
1
+ import { PrivateKey, PublicKey, SharedSecret } from "./crypto";
2
+ import { IPublicKey } from "./crypto.types";
3
+ import { NodeHash, IIdentity, ILocalIdentity } from "./identity.types";
4
+ import { DecryptedGroupData, DecryptedGroupText } from "./packet.types";
5
+ export declare const parseNodeHash: (hash: NodeHash | string | Uint8Array) => NodeHash;
6
+ export declare class Identity implements IIdentity {
7
+ publicKey: PublicKey;
8
+ constructor(publicKey: PublicKey | Uint8Array | string);
9
+ hash(): NodeHash;
10
+ toString(): string;
11
+ verify(signature: Uint8Array, message: Uint8Array): boolean;
12
+ matches(other: Identity | PublicKey | Uint8Array | string): boolean;
13
+ }
14
+ export declare class LocalIdentity extends Identity implements ILocalIdentity {
15
+ private privateKey;
16
+ constructor(privateKey: PrivateKey | Uint8Array | string, publicKey: PublicKey | Uint8Array | string);
17
+ sign(message: Uint8Array): Uint8Array;
18
+ calculateSharedSecret(other: IIdentity | IPublicKey): SharedSecret;
19
+ }
20
+ export declare class Contact {
21
+ name: string;
22
+ identity: Identity;
23
+ constructor(name: string, identity: Identity | PublicKey | Uint8Array | string);
24
+ matches(hash: Uint8Array | PublicKey): boolean;
25
+ publicKey(): PublicKey;
26
+ calculateSharedSecret(me: LocalIdentity): SharedSecret;
27
+ }
28
+ export declare class Group {
29
+ name: string;
30
+ private secret;
31
+ constructor(name: string, secret?: SharedSecret);
32
+ hash(): NodeHash;
33
+ decryptText(hmac: Uint8Array, ciphertext: Uint8Array): DecryptedGroupText;
34
+ encryptText(plain: DecryptedGroupText): {
35
+ hmac: Uint8Array;
36
+ ciphertext: Uint8Array;
37
+ };
38
+ decryptData(hmac: Uint8Array, ciphertext: Uint8Array): DecryptedGroupData;
39
+ encryptData(plain: DecryptedGroupData): {
40
+ hmac: Uint8Array;
41
+ ciphertext: Uint8Array;
42
+ };
43
+ }
44
+ export declare class Contacts {
45
+ private localIdentities;
46
+ private contacts;
47
+ private groups;
48
+ addLocalIdentity(identity: LocalIdentity): void;
49
+ addContact(contact: Contact): void;
50
+ decrypt(src: NodeHash | PublicKey, dst: NodeHash, hmac: Uint8Array, ciphertext: Uint8Array): {
51
+ localIdentity: LocalIdentity;
52
+ contact: Contact;
53
+ decrypted: Uint8Array;
54
+ };
55
+ private calculateSharedSecret;
56
+ addGroup(group: Group): void;
57
+ decryptGroupText(channelHash: NodeHash, hmac: Uint8Array, ciphertext: Uint8Array): {
58
+ decrypted: DecryptedGroupText;
59
+ group: Group;
60
+ };
61
+ decryptGroupData(channelHash: NodeHash, hmac: Uint8Array, ciphertext: Uint8Array): {
62
+ decrypted: DecryptedGroupData;
63
+ group: Group;
64
+ };
65
+ }
@@ -0,0 +1,302 @@
1
+ import { PrivateKey, PublicKey, SharedSecret } from "./crypto";
2
+ import { hexToBytes, BufferReader, BufferWriter } from "./parser";
3
+ export const parseNodeHash = (hash) => {
4
+ if (hash instanceof Uint8Array) {
5
+ return hash[0];
6
+ }
7
+ if (typeof hash === "number") {
8
+ if (hash < 0 || hash > 255) {
9
+ throw new Error("NodeHash number must be between 0x00 and 0xFF");
10
+ }
11
+ return hash;
12
+ }
13
+ else if (typeof hash === "string") {
14
+ const parsed = hexToBytes(hash);
15
+ if (parsed.length !== 1) {
16
+ throw new Error("NodeHash string must represent a single byte");
17
+ }
18
+ return parsed[0];
19
+ }
20
+ throw new Error("Invalid NodeHash type");
21
+ };
22
+ const toPublicKeyBytes = (key) => {
23
+ if (key instanceof Identity) {
24
+ return key.publicKey.toBytes();
25
+ }
26
+ else if (key instanceof PublicKey) {
27
+ return key.toBytes();
28
+ }
29
+ else if (key instanceof Uint8Array) {
30
+ return key;
31
+ }
32
+ else if (typeof key === 'string') {
33
+ return hexToBytes(key);
34
+ }
35
+ else {
36
+ throw new Error('Invalid type for toPublicKeyBytes');
37
+ }
38
+ };
39
+ export class Identity {
40
+ constructor(publicKey) {
41
+ if (publicKey instanceof PublicKey) {
42
+ this.publicKey = publicKey;
43
+ }
44
+ else if (publicKey instanceof Uint8Array || typeof publicKey === 'string') {
45
+ this.publicKey = new PublicKey(publicKey);
46
+ }
47
+ else {
48
+ throw new Error('Invalid type for Identity constructor');
49
+ }
50
+ }
51
+ hash() {
52
+ return this.publicKey.toHash();
53
+ }
54
+ toString() {
55
+ return this.publicKey.toString();
56
+ }
57
+ verify(signature, message) {
58
+ return this.publicKey.verify(message, signature);
59
+ }
60
+ matches(other) {
61
+ return this.publicKey.equals(toPublicKeyBytes(other));
62
+ }
63
+ }
64
+ export class LocalIdentity extends Identity {
65
+ constructor(privateKey, publicKey) {
66
+ if (publicKey instanceof PublicKey) {
67
+ super(publicKey.toBytes());
68
+ }
69
+ else {
70
+ super(publicKey);
71
+ }
72
+ if (privateKey instanceof PrivateKey) {
73
+ this.privateKey = privateKey;
74
+ }
75
+ else {
76
+ this.privateKey = new PrivateKey(privateKey);
77
+ }
78
+ }
79
+ sign(message) {
80
+ return this.privateKey.sign(message);
81
+ }
82
+ calculateSharedSecret(other) {
83
+ let otherPublicKey;
84
+ if (other instanceof Identity) {
85
+ otherPublicKey = other.publicKey;
86
+ }
87
+ else if ('toBytes' in other) {
88
+ otherPublicKey = new PublicKey(other.toBytes());
89
+ }
90
+ else if ('publicKey' in other && other.publicKey instanceof Uint8Array) {
91
+ otherPublicKey = new PublicKey(other.publicKey);
92
+ }
93
+ else if ('publicKey' in other && other.publicKey instanceof PublicKey) {
94
+ otherPublicKey = other.publicKey;
95
+ }
96
+ else if ('publicKey' in other && typeof other.publicKey === 'function') {
97
+ otherPublicKey = new PublicKey(other.publicKey().toBytes());
98
+ }
99
+ else {
100
+ throw new Error('Invalid type for calculateSharedSecret comparison');
101
+ }
102
+ return new SharedSecret(this.privateKey.calculateSharedSecret(otherPublicKey));
103
+ }
104
+ }
105
+ export class Contact {
106
+ constructor(name, identity) {
107
+ this.name = "";
108
+ this.name = name;
109
+ if (identity instanceof Identity) {
110
+ this.identity = identity;
111
+ }
112
+ else if (identity instanceof PublicKey) {
113
+ this.identity = new Identity(identity);
114
+ }
115
+ else if (identity instanceof Uint8Array || typeof identity === 'string') {
116
+ this.identity = new Identity(identity);
117
+ }
118
+ else {
119
+ throw new Error('Invalid type for Contact constructor');
120
+ }
121
+ }
122
+ matches(hash) {
123
+ return this.identity.publicKey.equals(hash);
124
+ }
125
+ publicKey() {
126
+ return this.identity.publicKey;
127
+ }
128
+ calculateSharedSecret(me) {
129
+ return me.calculateSharedSecret(this.identity);
130
+ }
131
+ }
132
+ export class Group {
133
+ constructor(name, secret) {
134
+ this.name = name;
135
+ if (secret) {
136
+ this.secret = secret;
137
+ }
138
+ else {
139
+ this.secret = SharedSecret.fromName(name);
140
+ }
141
+ }
142
+ hash() {
143
+ return this.secret.toHash();
144
+ }
145
+ decryptText(hmac, ciphertext) {
146
+ const data = this.secret.decrypt(hmac, ciphertext);
147
+ if (data.length < 5) {
148
+ throw new Error("Invalid ciphertext");
149
+ }
150
+ const reader = new BufferReader(data);
151
+ const timestamp = reader.readTimestamp();
152
+ const flags = reader.readByte();
153
+ const textType = (flags >> 2) & 0x3F;
154
+ const attempt = flags & 0x03;
155
+ const message = new TextDecoder('utf-8').decode(reader.readBytes());
156
+ return {
157
+ timestamp,
158
+ textType,
159
+ attempt,
160
+ message
161
+ };
162
+ }
163
+ encryptText(plain) {
164
+ const writer = new BufferWriter();
165
+ writer.writeTimestamp(plain.timestamp);
166
+ const flags = ((plain.textType & 0x3F) << 2) | (plain.attempt & 0x03);
167
+ writer.writeByte(flags);
168
+ writer.writeBytes(new TextEncoder().encode(plain.message));
169
+ const data = writer.toBytes();
170
+ return this.secret.encrypt(data);
171
+ }
172
+ decryptData(hmac, ciphertext) {
173
+ const data = this.secret.decrypt(hmac, ciphertext);
174
+ if (data.length < 4) {
175
+ throw new Error("Invalid ciphertext");
176
+ }
177
+ const reader = new BufferReader(data);
178
+ return {
179
+ timestamp: reader.readTimestamp(),
180
+ data: reader.readBytes(reader.remainingBytes())
181
+ };
182
+ }
183
+ encryptData(plain) {
184
+ const writer = new BufferWriter();
185
+ writer.writeTimestamp(plain.timestamp);
186
+ writer.writeBytes(plain.data);
187
+ const data = writer.toBytes();
188
+ return this.secret.encrypt(data);
189
+ }
190
+ }
191
+ export class Contacts {
192
+ constructor() {
193
+ this.localIdentities = [];
194
+ this.contacts = {};
195
+ this.groups = {};
196
+ }
197
+ addLocalIdentity(identity) {
198
+ this.localIdentities.push({ identity, sharedSecrets: {} });
199
+ }
200
+ addContact(contact) {
201
+ const hash = parseNodeHash(contact.identity.hash());
202
+ if (!this.contacts[hash]) {
203
+ this.contacts[hash] = [];
204
+ }
205
+ this.contacts[hash].push(contact);
206
+ }
207
+ decrypt(src, dst, hmac, ciphertext) {
208
+ // Find the public key associated with the source hash.
209
+ let contacts = [];
210
+ if (src instanceof PublicKey) {
211
+ // Check if we have a contact with this exact public key (for direct messages).
212
+ const srcHash = parseNodeHash(src.toHash());
213
+ for (const contact of this.contacts[srcHash] || []) {
214
+ if (contact.identity.matches(src)) {
215
+ contacts.push(contact);
216
+ }
217
+ }
218
+ // If no contact matches the public key, add a temporary contact with the hash and no name.
219
+ if (contacts.length === 0) {
220
+ contacts.push(new Contact("", new Identity(src.toBytes())));
221
+ }
222
+ }
223
+ else {
224
+ const srcHash = parseNodeHash(src);
225
+ contacts = this.contacts[srcHash] || [];
226
+ }
227
+ if (contacts.length === 0) {
228
+ throw new Error("Unknown source hash");
229
+ }
230
+ // Find the local identity associated with the destination hash.
231
+ const dstHash = parseNodeHash(dst);
232
+ const localIdentities = this.localIdentities.filter(li => li.identity.publicKey.key[0] === dstHash);
233
+ if (localIdentities.length === 0) {
234
+ throw new Error("Unknown destination hash");
235
+ }
236
+ // Try to decrypt with each combination of local identity and their public key.
237
+ for (const localIdentity of localIdentities) {
238
+ for (const contact of contacts) {
239
+ const sharedSecret = this.calculateSharedSecret(localIdentity, contact);
240
+ try {
241
+ const decrypted = sharedSecret.decrypt(hmac, ciphertext);
242
+ return { localIdentity: localIdentity.identity, contact, decrypted };
243
+ }
244
+ catch {
245
+ // Ignore decryption errors and try the next combination.
246
+ }
247
+ }
248
+ }
249
+ throw new Error("Decryption failed with all known identities and contacts");
250
+ }
251
+ // Caches the calculated shared secret for a given local identity and contact to avoid redundant calculations.
252
+ calculateSharedSecret(localIdentity, contact) {
253
+ const cacheKey = contact.identity.toString();
254
+ if (localIdentity.sharedSecrets[cacheKey]) {
255
+ return localIdentity.sharedSecrets[cacheKey];
256
+ }
257
+ const sharedSecret = localIdentity.identity.calculateSharedSecret(contact.identity);
258
+ localIdentity.sharedSecrets[cacheKey] = sharedSecret;
259
+ return sharedSecret;
260
+ }
261
+ addGroup(group) {
262
+ const hash = parseNodeHash(group.hash());
263
+ if (!this.groups[hash]) {
264
+ this.groups[hash] = [];
265
+ }
266
+ this.groups[hash].push(group);
267
+ }
268
+ decryptGroupText(channelHash, hmac, ciphertext) {
269
+ const hash = parseNodeHash(channelHash);
270
+ const groups = this.groups[hash] || [];
271
+ if (groups.length === 0) {
272
+ throw new Error("Unknown group hash");
273
+ }
274
+ for (const group of groups) {
275
+ try {
276
+ const decrypted = group.decryptText(hmac, ciphertext);
277
+ return { decrypted, group };
278
+ }
279
+ catch {
280
+ // Ignore decryption errors and try the next group.
281
+ }
282
+ }
283
+ throw new Error("Decryption failed with all known groups");
284
+ }
285
+ decryptGroupData(channelHash, hmac, ciphertext) {
286
+ const hash = parseNodeHash(channelHash);
287
+ const groups = this.groups[hash] || [];
288
+ if (groups.length === 0) {
289
+ throw new Error("Unknown group hash");
290
+ }
291
+ for (const group of groups) {
292
+ try {
293
+ const decrypted = group.decryptData(hmac, ciphertext);
294
+ return { decrypted, group };
295
+ }
296
+ catch {
297
+ // Ignore decryption errors and try the next group.
298
+ }
299
+ }
300
+ throw new Error("Decryption failed with all known groups");
301
+ }
302
+ }
@@ -0,0 +1,17 @@
1
+ import { IPublicKey, ISharedSecret } from "./crypto.types";
2
+ export type NodeHash = number;
3
+ export interface IIdentity {
4
+ hash(): NodeHash;
5
+ toString(): string;
6
+ verify(signature: Uint8Array, message: Uint8Array): boolean;
7
+ matches(other: IIdentity | IPublicKey | Uint8Array | string): boolean;
8
+ }
9
+ export interface ILocalIdentity extends IIdentity {
10
+ sign(message: Uint8Array): Uint8Array;
11
+ calculateSharedSecret(other: IIdentity | IPublicKey): ISharedSecret;
12
+ }
13
+ export interface IContact {
14
+ name: string;
15
+ identity: IIdentity;
16
+ calculateSharedSecret(me: ILocalIdentity): ISharedSecret;
17
+ }
@@ -0,0 +1 @@
1
+ export {};