@hive-p2p/browser 1.0.69 → 1.0.71
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/core/config.mjs +4 -0
- package/core/crypto-codex.mjs +47 -12
- package/core/node.mjs +77 -12
- package/core/peer-store.mjs +8 -0
- package/core/topologist.mjs +1 -1
- package/core/unicast.mjs +7 -6
- package/hive-p2p.min.js +3 -3
- package/package.json +1 -1
package/core/config.mjs
CHANGED
package/core/crypto-codex.mjs
CHANGED
|
@@ -3,7 +3,8 @@ import { SIMULATION, NODE, IDENTITY, GOSSIP, UNICAST, LOG_CSS } from './config.m
|
|
|
3
3
|
import { GossipMessage } from './gossip.mjs';
|
|
4
4
|
import { DirectMessage, ReroutedDirectMessage } from './unicast.mjs';
|
|
5
5
|
import { Converter } from '../services/converter.mjs';
|
|
6
|
-
import { ed25519, Argon2Unified } from '../services/cryptos.mjs'; // now exposed in full and browser builds
|
|
6
|
+
import { ed25519, x25519, chacha20poly1305, randomBytes , Argon2Unified } from '../services/cryptos.mjs'; // now exposed in full and browser builds
|
|
7
|
+
import { concatBytes } from '@noble/ciphers/utils.js';
|
|
7
8
|
|
|
8
9
|
export class CryptoCodex {
|
|
9
10
|
argon2 = new Argon2Unified();
|
|
@@ -52,6 +53,7 @@ export class CryptoCodex {
|
|
|
52
53
|
const { bitsString } = await this.argon2.hash(publicKey, 'HiveP2P', IDENTITY.ARGON2_MEM) || {};
|
|
53
54
|
if (bitsString && bitsString.startsWith('0'.repeat(IDENTITY.DIFFICULTY))) return true;
|
|
54
55
|
}
|
|
56
|
+
/** @param {Uint8Array} publicKey */
|
|
55
57
|
#idFromPublicKey(publicKey) {
|
|
56
58
|
if (IDENTITY.ARE_IDS_HEX) return this.converter.bytesToHex(publicKey.slice(0, this.idLength), IDENTITY.ID_LENGTH);
|
|
57
59
|
return this.converter.bytesToString(publicKey.slice(0, IDENTITY.ID_LENGTH));
|
|
@@ -74,7 +76,32 @@ export class CryptoCodex {
|
|
|
74
76
|
if (this.verbose > 0) console.log(`%cFAILED to generate id after ${maxIterations} iterations. Try lowering the difficulty.`, LOG_CSS.CRYPTO_CODEX);
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
//
|
|
79
|
+
// PRIVACY
|
|
80
|
+
generateEphemeralX25519Keypair() {
|
|
81
|
+
const { secretKey, publicKey } = x25519.keygen();
|
|
82
|
+
return { myPub: publicKey, myPriv: secretKey };
|
|
83
|
+
}
|
|
84
|
+
computeX25519SharedSecret(secret, pub) {
|
|
85
|
+
return x25519.getSharedSecret(secret, pub);
|
|
86
|
+
}
|
|
87
|
+
/** @param {Uint8Array} data @param {Uint8Array} sharedSecret */
|
|
88
|
+
encryptData(data, sharedSecret) {
|
|
89
|
+
if (!sharedSecret) throw new Error('Cannot encrypt data without shared secret.');
|
|
90
|
+
const nonce = randomBytes(12);
|
|
91
|
+
const cipher = chacha20poly1305(sharedSecret, nonce);
|
|
92
|
+
const encrypted = cipher.encrypt(data);
|
|
93
|
+
return concatBytes(nonce, encrypted);
|
|
94
|
+
}
|
|
95
|
+
/** @param {Uint8Array} encryptedData @param {Uint8Array} sharedSecret */
|
|
96
|
+
decryptData(encryptedData, sharedSecret) {
|
|
97
|
+
if (!sharedSecret) throw new Error('Cannot decrypt data without shared secret.');
|
|
98
|
+
const nonce = encryptedData.slice(0, 12);
|
|
99
|
+
const cipher = chacha20poly1305(sharedSecret, nonce);
|
|
100
|
+
const decrypted = cipher.decrypt(encryptedData.slice(12));
|
|
101
|
+
return decrypted;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// MESSAGE CREATION (SERIALIZATION AND SIGNATURE INCLUDED)
|
|
78
105
|
/** @param {Uint8Array} bufferView @param {Uint8Array} privateKey @param {number} [signaturePosition] */
|
|
79
106
|
signBufferViewAndAppendSignature(bufferView, privateKey, signaturePosition = bufferView.length - IDENTITY.SIGNATURE_LENGTH) {
|
|
80
107
|
if (this.AVOID_CRYPTO) return;
|
|
@@ -107,8 +134,8 @@ export class CryptoCodex {
|
|
|
107
134
|
clone[serializedMessage.length - 1] = Math.max(0, hops - 1);
|
|
108
135
|
return clone;
|
|
109
136
|
}
|
|
110
|
-
/** @param {string} type @param {string | Uint8Array | Object} data @param {string[]} route @param {string[]} [neighbors] */
|
|
111
|
-
createUnicastMessage(type, data, route, neighbors = [], timestamp = CLOCK.time) {
|
|
137
|
+
/** @param {string} type @param {string | Uint8Array | Object} data @param {string[]} route @param {string[]} [neighbors] @param {Uint8Array} [encryptionKey] @param {number} [timestamp] */
|
|
138
|
+
createUnicastMessage(type, data, route, neighbors = [], encryptionKey, timestamp = CLOCK.time) {
|
|
112
139
|
const MARKER = UNICAST.MARKERS_BYTES[type];
|
|
113
140
|
if (MARKER === undefined) throw new Error(`Failed to create unicast message: unknown type '${type}'.`);
|
|
114
141
|
if (route.length < 2) throw new Error('Failed to create unicast message: route must have at least 2 nodes (next hop and target).');
|
|
@@ -116,15 +143,16 @@ export class CryptoCodex {
|
|
|
116
143
|
|
|
117
144
|
const neighborsBytes = this.#idsToBytes(neighbors);
|
|
118
145
|
const { dataCode, dataBytes } = this.#dataToBytes(data);
|
|
146
|
+
const dBytes = encryptionKey ? this.encryptData(dataBytes, encryptionKey) : dataBytes;
|
|
119
147
|
const routeBytes = this.#idsToBytes(route);
|
|
120
|
-
const totalBytes = 1 + 1 + 1 + 8 + 4 + 32 + neighborsBytes.length + 1 + routeBytes.length +
|
|
148
|
+
const totalBytes = 1 + 1 + 1 + 8 + 4 + 32 + neighborsBytes.length + 1 + routeBytes.length + dBytes.length + IDENTITY.SIGNATURE_LENGTH;
|
|
121
149
|
const buffer = new ArrayBuffer(totalBytes);
|
|
122
150
|
const bufferView = new Uint8Array(buffer);
|
|
123
|
-
this.#setBufferHeader(bufferView, MARKER, dataCode, neighbors.length, timestamp,
|
|
151
|
+
this.#setBufferHeader(bufferView, MARKER, dataCode, neighbors.length, timestamp, dBytes, this.publicKey);
|
|
124
152
|
|
|
125
|
-
const NDBL = neighborsBytes.length +
|
|
153
|
+
const NDBL = neighborsBytes.length + dBytes.length;
|
|
126
154
|
bufferView.set(neighborsBytes, 47); // X bytes for neighbors
|
|
127
|
-
bufferView.set(
|
|
155
|
+
bufferView.set(dBytes, 47 + neighborsBytes.length); // X bytes for data
|
|
128
156
|
bufferView.set([route.length], 47 + NDBL); // 1 byte for route length
|
|
129
157
|
bufferView.set(routeBytes, 47 + 1 + NDBL); // X bytes for route
|
|
130
158
|
|
|
@@ -210,8 +238,8 @@ export class CryptoCodex {
|
|
|
210
238
|
} catch (error) { if (this.verbose > 1) console.warn(`Error deserializing ${topic || 'unknown'} gossip message:`, error.stack); }
|
|
211
239
|
return null;
|
|
212
240
|
}
|
|
213
|
-
/** @param {Uint8Array | ArrayBuffer} serialized @return {DirectMessage | ReroutedDirectMessage | null} */
|
|
214
|
-
readUnicastMessage(serialized) {
|
|
241
|
+
/** @param {Uint8Array | ArrayBuffer} serialized @param {import('./peer-store.mjs').PeerStore} peerStore @return {DirectMessage | ReroutedDirectMessage | null} */
|
|
242
|
+
readUnicastMessage(serialized, peerStore) {
|
|
215
243
|
if (this.verbose > 3) console.log(`%creadUnicastMessage ${serialized.byteLength} bytes`, LOG_CSS.CRYPTO_CODEX);
|
|
216
244
|
if (this.verbose > 4) console.log(`%c${serialized}`, LOG_CSS.CRYPTO_CODEX);
|
|
217
245
|
try { // 1, 1, 1, 8, 4, 32, X, 1, X, 64
|
|
@@ -220,12 +248,18 @@ export class CryptoCodex {
|
|
|
220
248
|
if (type === undefined) throw new Error(`Failed to deserialize unicast message: unknown marker byte ${d[0]}.`);
|
|
221
249
|
const NDBL = neighLength + dataLength;
|
|
222
250
|
const neighbors = this.#bytesToIds(serialized.slice(47, 47 + neighLength));
|
|
223
|
-
const deserializedData = this.#bytesToData(dataCode, serialized.slice(47 + neighLength, 47 + NDBL));
|
|
224
251
|
const routeLength = serialized[47 + NDBL];
|
|
225
252
|
const routeBytesLength = routeLength * this.idLength;
|
|
226
253
|
const signatureStart = 47 + NDBL + 1 + routeBytesLength;
|
|
227
254
|
const routeBytes = serialized.slice(47 + NDBL + 1, signatureStart);
|
|
228
255
|
const route = this.#bytesToIds(routeBytes);
|
|
256
|
+
|
|
257
|
+
const destId = route[route.length - 1];
|
|
258
|
+
const d = type === 'private_message' && this.id === destId
|
|
259
|
+
? this.decryptData(serialized.slice(47 + neighLength, 47 + NDBL), peerStore.privacy[this.#idFromPublicKey(pubkey)]?.sharedSecret)
|
|
260
|
+
: serialized.slice(47 + neighLength, 47 + NDBL);
|
|
261
|
+
|
|
262
|
+
const deserializedData = this.id === destId ? this.#bytesToData(dataCode, d) : d;
|
|
229
263
|
const initialMessageEnd = signatureStart + IDENTITY.SIGNATURE_LENGTH;
|
|
230
264
|
const signature = serialized.slice(signatureStart, initialMessageEnd);
|
|
231
265
|
const isPatched = (serialized.length > initialMessageEnd);
|
|
@@ -243,7 +277,8 @@ export class CryptoCodex {
|
|
|
243
277
|
#bytesToIds(serialized) {
|
|
244
278
|
const ids = [];
|
|
245
279
|
const idLength = this.idLength;
|
|
246
|
-
if (serialized.length % idLength !== 0)
|
|
280
|
+
if (serialized.length % idLength !== 0)
|
|
281
|
+
throw new Error('Failed to parse ids: invalid serialized length.');
|
|
247
282
|
|
|
248
283
|
for (let i = 0; i < serialized.length / idLength; i++) {
|
|
249
284
|
const idBytes = serialized.slice(i * idLength, (i + 1) * idLength);
|
package/core/node.mjs
CHANGED
|
@@ -17,9 +17,9 @@ import { NodeServices } from './node-services.mjs';
|
|
|
17
17
|
* @param {string} [options.domain] If provided, the node will operate as a public node and start necessary services (e.g., WebSocket server)
|
|
18
18
|
* @param {number} [options.port] If provided, the node will listen on this port (default: SERVICE.PORT)
|
|
19
19
|
* @param {number} [options.verbose] Verbosity level for logging (default: NODE.DEFAULT_VERBOSE) */
|
|
20
|
-
export async function createPublicNode(options) {
|
|
20
|
+
export async function createPublicNode(options = {}) {
|
|
21
21
|
const verbose = options.verbose !== undefined ? options.verbose : NODE.DEFAULT_VERBOSE;
|
|
22
|
-
const domain = options.domain ||
|
|
22
|
+
const domain = options.domain || "localhost";
|
|
23
23
|
const codex = options.cryptoCodex || new CryptoCodex(undefined, verbose);
|
|
24
24
|
const clockSync = CLOCK.sync(verbose);
|
|
25
25
|
if (!codex.publicKey) await codex.generate(domain ? true : false);
|
|
@@ -82,7 +82,7 @@ export class Node {
|
|
|
82
82
|
const { arbiter, peerStore, messager, gossip, topologist } = this;
|
|
83
83
|
|
|
84
84
|
// SETUP TRANSPORTS LISTENERS
|
|
85
|
-
peerStore.on('signal', (peerId, data) => this.sendMessage(peerId, data, 'signal_answer')); // answer created => send it to offerer
|
|
85
|
+
peerStore.on('signal', (peerId, data) => this.sendMessage(peerId, data, { type: 'signal_answer' })); // answer created => send it to offerer
|
|
86
86
|
peerStore.on('connect', (peerId, direction) => this.#onConnect(peerId, direction));
|
|
87
87
|
peerStore.on('disconnect', (peerId, direction) => this.#onDisconnect(peerId, direction));
|
|
88
88
|
peerStore.on('data', (peerId, data) => this.#onData(peerId, data));
|
|
@@ -90,6 +90,7 @@ export class Node {
|
|
|
90
90
|
// UNICAST LISTENERS
|
|
91
91
|
messager.on('signal_answer', (senderId, data) => topologist.handleIncomingSignal(senderId, data));
|
|
92
92
|
messager.on('signal_offer', (senderId, data) => topologist.handleIncomingSignal(senderId, data));
|
|
93
|
+
messager.on('privacy', (senderId, data) => this.#handlePrivacy(senderId, data));
|
|
93
94
|
|
|
94
95
|
// GOSSIP LISTENERS
|
|
95
96
|
gossip.on('signal_offer', (senderId, data, HOPS) => topologist.handleIncomingSignal(senderId, data, HOPS));
|
|
@@ -108,9 +109,9 @@ export class Node {
|
|
|
108
109
|
|
|
109
110
|
const isHoverNeighbored = this.peerStore.neighborsList.length >= DISCOVERY.TARGET_NEIGHBORS_COUNT + this.halfTarget;
|
|
110
111
|
const dispatchEvents = () => {
|
|
111
|
-
//this.sendMessage(peerId, this.id, 'handshake'); // send it in both case, no doubt...
|
|
112
|
+
//this.sendMessage(peerId, this.id, { type: 'handshake' }); // send it in both case, no doubt...
|
|
112
113
|
if (DISCOVERY.ON_CONNECT_DISPATCH.OVER_NEIGHBORED && isHoverNeighbored)
|
|
113
|
-
this.broadcast([], 'over_neighbored'); // inform my neighbors that I am over neighbored
|
|
114
|
+
this.broadcast([], { type: 'over_neighbored' }); // inform my neighbors that I am over neighbored
|
|
114
115
|
if (DISCOVERY.ON_CONNECT_DISPATCH.SHARE_HISTORY)
|
|
115
116
|
if (this.peerStore.getUpdatedPeerConnectionsCount(peerId) <= 1) this.gossip.sendGossipHistoryToPeer(peerId);
|
|
116
117
|
};
|
|
@@ -160,12 +161,69 @@ export class Node {
|
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
/** Broadcast a message to all connected peers or to a specified peer
|
|
163
|
-
* @param {string | Uint8Array | Object} data @param {string}
|
|
164
|
-
* @param {number} [timestamp] default: CLOCK.time
|
|
165
|
-
|
|
164
|
+
* @param {string | Uint8Array | Object} data @param {string} [targetId] default: broadcast to all
|
|
165
|
+
* @param {number} [timestamp] default: CLOCK.time
|
|
166
|
+
* @param {Object} [options]
|
|
167
|
+
* @param {string} [options.topic] default: 'gossip'
|
|
168
|
+
* @param {number} [options.HOPS] default: GOSSIP.HOPS[topic] || GOSSIP.HOPS.default */
|
|
169
|
+
broadcast(data, options = {}) {
|
|
170
|
+
const { topic, HOPS } = options;
|
|
171
|
+
return this.gossip.broadcastToAll(data, topic, HOPS);
|
|
172
|
+
}
|
|
166
173
|
|
|
167
|
-
/**
|
|
168
|
-
|
|
174
|
+
/** Send a unicast message to a specific peer
|
|
175
|
+
* @param {string} remoteId @param {string | Uint8Array | Object} data
|
|
176
|
+
* @param {Object} [options]
|
|
177
|
+
* @param {'message' | 'private_message'} [options.type] default: 'message'
|
|
178
|
+
* @param {number} [options.spread=1] Number of neighbors used to relay the message */
|
|
179
|
+
async sendMessage(remoteId, data, options = {}) {
|
|
180
|
+
const { type, spread } = options;
|
|
181
|
+
const privacy = type === 'private_message' ? await this.getPeerPrivacy(remoteId) : null;
|
|
182
|
+
if (privacy && !privacy?.sharedSecret) {
|
|
183
|
+
this.verbose > 1 ? console.warn(`Cannot send encrypted message to ${remoteId} as shared secret could not be established.`) : null;
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return this.messager.sendUnicast(remoteId, data, type, spread);
|
|
188
|
+
}
|
|
189
|
+
/** Send a private message to a specific peer, ensuring shared secret is established
|
|
190
|
+
* @param {string} remoteId @param {string | Uint8Array | Object} data
|
|
191
|
+
* @param {Object} [options]
|
|
192
|
+
* @param {number} [options.spread=1] Number of neighbors used to relay the message */
|
|
193
|
+
async sendPrivateMessage(remoteId, data, options = {}) {
|
|
194
|
+
const o = { ...options, type: 'private_message' };
|
|
195
|
+
return this.sendMessage(remoteId, data, o);
|
|
196
|
+
}
|
|
197
|
+
// Building this function, she can be moved to another class later
|
|
198
|
+
async getPeerPrivacy(peerId, retry = 20) {
|
|
199
|
+
let p = this.peerStore.privacy[peerId];
|
|
200
|
+
if (!p) {
|
|
201
|
+
this.peerStore.privacy[peerId] = this.cryptoCodex.generateEphemeralX25519Keypair();
|
|
202
|
+
p = this.peerStore.privacy[peerId];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let nextMessageIn = 1;
|
|
206
|
+
while (!p?.sharedSecret && retry-- > 0) {
|
|
207
|
+
if (nextMessageIn-- <= 0) {
|
|
208
|
+
this.messager.sendUnicast(peerId, p.myPub, 'privacy');
|
|
209
|
+
nextMessageIn = 5;
|
|
210
|
+
}
|
|
211
|
+
await new Promise(r => setTimeout(r, 100));
|
|
212
|
+
p = this.peerStore.privacy[peerId];
|
|
213
|
+
}
|
|
214
|
+
return p;
|
|
215
|
+
}
|
|
216
|
+
/** @param {string} senderId @param {Uint8Array} peerPub */
|
|
217
|
+
#handlePrivacy(senderId, peerPub) {
|
|
218
|
+
if (this.peerStore.privacy[senderId]?.sharedSecret) return; // already have shared secret
|
|
219
|
+
|
|
220
|
+
if (!this.peerStore.privacy[senderId])
|
|
221
|
+
this.peerStore.privacy[senderId] = this.cryptoCodex.generateEphemeralX25519Keypair();
|
|
222
|
+
|
|
223
|
+
this.peerStore.privacy[senderId].peerPub = peerPub;
|
|
224
|
+
this.peerStore.privacy[senderId].sharedSecret = this.cryptoCodex.computeX25519SharedSecret(this.peerStore.privacy[senderId].myPriv, peerPub);
|
|
225
|
+
this.messager.sendUnicast(senderId, this.peerStore.privacy[senderId].myPub, 'privacy');
|
|
226
|
+
}
|
|
169
227
|
|
|
170
228
|
/** Send a connection request to a peer */
|
|
171
229
|
async tryConnectToPeer(targetId = 'toto', retry = 10) {
|
|
@@ -174,7 +232,7 @@ export class Node {
|
|
|
174
232
|
const { offerHash, readyOffer } = this.peerStore.offerManager.bestReadyOffer(100, false);
|
|
175
233
|
if (!offerHash || !readyOffer) await new Promise(r => setTimeout(r, 1000)); // build in progress...
|
|
176
234
|
else {
|
|
177
|
-
this.messager.sendUnicast(targetId, { signal: readyOffer.signal, offerHash }, 'signal_offer'
|
|
235
|
+
this.messager.sendUnicast(targetId, { signal: readyOffer.signal, offerHash }, 'signal_offer');
|
|
178
236
|
return;
|
|
179
237
|
}
|
|
180
238
|
} while (retry-- > 0);
|
|
@@ -201,7 +259,14 @@ export class Node {
|
|
|
201
259
|
|
|
202
260
|
/** Triggered when a new message is received.
|
|
203
261
|
* @param {function} callback can use arguments: (senderId:string, data:any) */
|
|
204
|
-
onMessageData(callback
|
|
262
|
+
onMessageData(callback, includesPrivate = true) {
|
|
263
|
+
this.messager.on('message', callback);
|
|
264
|
+
if (includesPrivate) this.messager.on('private_message', callback);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Triggered when a new private message is received.
|
|
268
|
+
* @param {function} callback can use arguments: (senderId:string, data:any) */
|
|
269
|
+
onPrivateMessageData(callback) { this.messager.on('private_message', callback); }
|
|
205
270
|
|
|
206
271
|
/** Triggered when a new gossip message is received.
|
|
207
272
|
* @param {function} callback can use arguments: (senderId:string, data:any, HOPS:number) */
|
package/core/peer-store.mjs
CHANGED
|
@@ -20,6 +20,13 @@ export class KnownPeer { // known peer, not necessarily connected
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
class Privacy {
|
|
24
|
+
/** @type {Uint8Array} */ sharedSecret;
|
|
25
|
+
/** @type {Uint8Array} */ myPub;
|
|
26
|
+
/** @type {Uint8Array} */ myPriv;
|
|
27
|
+
/** @type {Uint8Array} */ peerPub;
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
export class PeerConnection { // WebSocket or WebRTC connection wrapper
|
|
24
31
|
peerId; transportInstance; isWebSocket; direction; pendingUntil; connStartTime;
|
|
25
32
|
|
|
@@ -46,6 +53,7 @@ export class PeerStore { // Manages all peers informations and connections (WebS
|
|
|
46
53
|
/** @type {Record<string, PeerConnection>} */ connected = {};
|
|
47
54
|
/** @type {Record<string, KnownPeer>} */ known = {}; // known peers store
|
|
48
55
|
/** @type {number} */ knownCount = 0;
|
|
56
|
+
/** @type {Record<string, Privacy>} */ privacy = {}; // peerId, Privacy settings
|
|
49
57
|
/** @type {Record<string, number>} */ kick = {}; // peerId, timestamp until kick expires
|
|
50
58
|
/** @type {Record<string, Function[]>} */ callbacks = {
|
|
51
59
|
'connect': [(peerId, direction) => this.#handleConnect(peerId, direction)],
|
package/core/topologist.mjs
CHANGED
|
@@ -252,7 +252,7 @@ export class Topologist {
|
|
|
252
252
|
if (sentTo.has(peerId)) continue;
|
|
253
253
|
if (sentTo.size === 0) readyOffer.sentCounter++;
|
|
254
254
|
sentTo.set(peerId, true);
|
|
255
|
-
this.messager.sendUnicast(peerId, { signal: readyOffer.signal, offerHash }, 'signal_offer'
|
|
255
|
+
this.messager.sendUnicast(peerId, { signal: readyOffer.signal, offerHash }, 'signal_offer');
|
|
256
256
|
if (sentTo.size >= 12) break; // limit to 12 unicast max
|
|
257
257
|
}
|
|
258
258
|
}
|
package/core/unicast.mjs
CHANGED
|
@@ -75,14 +75,14 @@ export class UnicastMessager {
|
|
|
75
75
|
}
|
|
76
76
|
/** Send unicast message to a target
|
|
77
77
|
* @param {string} remoteId @param {string | Uint8Array | Object} data @param {string} type
|
|
78
|
-
* @param {number} [spread] Max neighbors used to relay the message, default: 1 */
|
|
78
|
+
* @param {number} [spread] *Optional* Max neighbors used to relay the message, default: 1 */
|
|
79
79
|
sendUnicast(remoteId, data, type = 'message', spread = 1) {
|
|
80
80
|
if (remoteId === this.id) return false;
|
|
81
81
|
|
|
82
82
|
const builtResult = this.pathFinder.buildRoutes(remoteId, this.maxRoutes, this.maxHops, this.maxNodes, true);
|
|
83
83
|
if (!builtResult.success) return false;
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
|
|
85
|
+
const encryptionKey = type === 'private_message' ? this.peerStore.privacy[remoteId]?.sharedSecret : undefined;
|
|
86
86
|
const finalSpread = builtResult.success === 'blind' ? 1 : spread; // Spread only if re-routing is false
|
|
87
87
|
for (let i = 0; i < Math.min(finalSpread, builtResult.routes.length); i++) {
|
|
88
88
|
const route = builtResult.routes[i].path;
|
|
@@ -91,7 +91,8 @@ export class UnicastMessager {
|
|
|
91
91
|
continue; // too long route
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
const msg = this.cryptoCodex.createUnicastMessage(type, data, route, this.peerStore.neighborsList, encryptionKey);
|
|
95
|
+
this.#sendMessageToPeer(route[1], msg);
|
|
95
96
|
}
|
|
96
97
|
return true;
|
|
97
98
|
}
|
|
@@ -112,7 +113,7 @@ export class UnicastMessager {
|
|
|
112
113
|
if (this.arbiter.isBanished(from)) return this.verbose >= 3 ? console.info(`%cReceived direct message from banned peer ${from}, ignoring.`, 'color: red;') : null;
|
|
113
114
|
if (!this.arbiter.countMessageBytes(from, serialized.byteLength, 'unicast')) return; // ignore if flooding/banished
|
|
114
115
|
|
|
115
|
-
const message = this.cryptoCodex.readUnicastMessage(serialized);
|
|
116
|
+
const message = this.cryptoCodex.readUnicastMessage(serialized, this.peerStore);
|
|
116
117
|
if (!message) return this.arbiter.countPeerAction(from, 'WRONG_SERIALIZATION');
|
|
117
118
|
const isOk = await this.arbiter.digestMessage(from, message, serialized);
|
|
118
119
|
if (!isOk) return; // invalid message or from banished peer
|
|
@@ -148,7 +149,7 @@ export class UnicastMessager {
|
|
|
148
149
|
return; // too long route
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
const patchedMessage =
|
|
152
|
+
const patchedMessage = this.cryptoCodex.createReroutedUnicastMessage(serialized, newRoute);
|
|
152
153
|
const nextPeerId = newRoute[selfPosition + 1];
|
|
153
154
|
this.#sendMessageToPeer(nextPeerId, patchedMessage);
|
|
154
155
|
}
|