@decentnetwork/peer 0.1.152 → 0.1.154
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/dist/compat/express.js +4 -3
- package/dist/compat/net-crypto.js +7 -6
- package/dist/crypto/shared-key.d.ts +4 -0
- package/dist/crypto/shared-key.js +45 -0
- package/dist/peer.js +52 -26
- package/package.json +1 -1
package/dist/compat/express.js
CHANGED
|
@@ -2,6 +2,7 @@ import { ByteBuffer, Encoding } from "flatbuffers";
|
|
|
2
2
|
import nacl from "tweetnacl";
|
|
3
3
|
import { base58ToBytes, bytesToBase58 } from "../utils/base58.js";
|
|
4
4
|
import { randomBytes } from "../utils/bytes.js";
|
|
5
|
+
import { sharedKey as sharedKeyFor } from "../crypto/shared-key.js";
|
|
5
6
|
const EXPRESS_MAGIC = 0xca6ee595;
|
|
6
7
|
const NONCE_SIZE = 24;
|
|
7
8
|
const HTTP_TIMEOUT_MS = 15000;
|
|
@@ -34,7 +35,7 @@ export class LegacyExpressClient {
|
|
|
34
35
|
host: node.host,
|
|
35
36
|
port: node.port,
|
|
36
37
|
tls: node.tls !== false, // default HTTPS; opt into HTTP with tls:false
|
|
37
|
-
sharedKey:
|
|
38
|
+
sharedKey: sharedKeyFor(expressPk, this.#selfKeyPair.secretKey)
|
|
38
39
|
};
|
|
39
40
|
});
|
|
40
41
|
}
|
|
@@ -49,7 +50,7 @@ export class LegacyExpressClient {
|
|
|
49
50
|
if (friendPk.length !== 32) {
|
|
50
51
|
throw new Error("friend user id must decode to 32-byte public key");
|
|
51
52
|
}
|
|
52
|
-
const friendSharedKey =
|
|
53
|
+
const friendSharedKey = sharedKeyFor(friendPk, this.#selfKeyPair.secretKey);
|
|
53
54
|
const friendEncrypted = encrypt(friendSharedKey, carrierPacket);
|
|
54
55
|
await this.#postEncrypted(friendUserId, friendEncrypted);
|
|
55
56
|
}
|
|
@@ -112,7 +113,7 @@ export class LegacyExpressClient {
|
|
|
112
113
|
if (friendPk.length !== 32) {
|
|
113
114
|
continue;
|
|
114
115
|
}
|
|
115
|
-
const friendSharedKey =
|
|
116
|
+
const friendSharedKey = sharedKeyFor(friendPk, this.#selfKeyPair.secretKey);
|
|
116
117
|
const packet = decrypt(friendSharedKey, msg.payload);
|
|
117
118
|
if (!packet) {
|
|
118
119
|
continue;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import nacl from "tweetnacl";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { concatBytes, randomBytes } from "../utils/bytes.js";
|
|
4
|
+
import { sharedKey as sharedKeyFor } from "../crypto/shared-key.js";
|
|
4
5
|
export const NET_PACKET_COOKIE_REQUEST = 0x18;
|
|
5
6
|
export const NET_PACKET_COOKIE_RESPONSE = 0x19;
|
|
6
7
|
export const NET_PACKET_CRYPTO_HS = 0x1a;
|
|
@@ -44,7 +45,7 @@ export function createCookieRequest(opts) {
|
|
|
44
45
|
throw new Error("cookie request plaintext size mismatch");
|
|
45
46
|
}
|
|
46
47
|
const nonce = randomBytes(NONCE_SIZE);
|
|
47
|
-
const sharedKey =
|
|
48
|
+
const sharedKey = sharedKeyFor(opts.receiverDhtPublicKey, opts.senderDhtSecretKey);
|
|
48
49
|
const cipher = nacl.secretbox(plain, nonce, sharedKey);
|
|
49
50
|
return concatBytes([
|
|
50
51
|
Uint8Array.of(NET_PACKET_COOKIE_REQUEST),
|
|
@@ -60,7 +61,7 @@ export function openCookieRequest(packet, opts) {
|
|
|
60
61
|
const senderDhtPublicKey = packet.slice(1, 1 + KEY_SIZE);
|
|
61
62
|
const nonce = packet.slice(1 + KEY_SIZE, 1 + KEY_SIZE + NONCE_SIZE);
|
|
62
63
|
const cipher = packet.slice(1 + KEY_SIZE + NONCE_SIZE);
|
|
63
|
-
const sharedKey =
|
|
64
|
+
const sharedKey = sharedKeyFor(senderDhtPublicKey, opts.receiverDhtSecretKey);
|
|
64
65
|
const plain = nacl.secretbox.open(cipher, nonce, sharedKey);
|
|
65
66
|
if (!plain || plain.length !== COOKIE_REQUEST_PLAIN_LENGTH) {
|
|
66
67
|
return undefined;
|
|
@@ -79,7 +80,7 @@ export function createCookieResponse(opts) {
|
|
|
79
80
|
});
|
|
80
81
|
const plain = concatBytes([cookie, writeUint64Le(opts.request.echo)]);
|
|
81
82
|
const nonce = randomBytes(NONCE_SIZE);
|
|
82
|
-
const sharedKey =
|
|
83
|
+
const sharedKey = sharedKeyFor(opts.request.senderDhtPublicKey, opts.receiverDhtSecretKey);
|
|
83
84
|
const cipher = nacl.secretbox(plain, nonce, sharedKey);
|
|
84
85
|
return concatBytes([Uint8Array.of(NET_PACKET_COOKIE_RESPONSE), nonce, cipher]);
|
|
85
86
|
}
|
|
@@ -89,7 +90,7 @@ export function openCookieResponse(packet, opts) {
|
|
|
89
90
|
}
|
|
90
91
|
const nonce = packet.slice(1, 1 + NONCE_SIZE);
|
|
91
92
|
const cipher = packet.slice(1 + NONCE_SIZE);
|
|
92
|
-
const sharedKey =
|
|
93
|
+
const sharedKey = sharedKeyFor(opts.receiverDhtPublicKey, opts.senderDhtSecretKey);
|
|
93
94
|
const plain = nacl.secretbox.open(cipher, nonce, sharedKey);
|
|
94
95
|
if (!plain || plain.length !== COOKIE_RESPONSE_PLAIN_LENGTH) {
|
|
95
96
|
return undefined;
|
|
@@ -135,7 +136,7 @@ export function createCryptoHandshake(opts) {
|
|
|
135
136
|
sha512(opts.recipientCookie),
|
|
136
137
|
ownCookie
|
|
137
138
|
]);
|
|
138
|
-
const sharedKey =
|
|
139
|
+
const sharedKey = sharedKeyFor(opts.receiverRealPublicKey, opts.senderRealSecretKey);
|
|
139
140
|
const innerCipher = nacl.secretbox(innerPlain, innerNonce, sharedKey);
|
|
140
141
|
return concatBytes([Uint8Array.of(NET_PACKET_CRYPTO_HS), opts.recipientCookie, innerNonce, innerCipher]);
|
|
141
142
|
}
|
|
@@ -152,7 +153,7 @@ export function openCryptoHandshake(packet, opts) {
|
|
|
152
153
|
const senderDhtPublicKey = cookieParsed.dhtPublicKey;
|
|
153
154
|
const nonce = packet.slice(1 + COOKIE_LENGTH, 1 + COOKIE_LENGTH + NONCE_SIZE);
|
|
154
155
|
const cipher = packet.slice(1 + COOKIE_LENGTH + NONCE_SIZE);
|
|
155
|
-
const sharedKey =
|
|
156
|
+
const sharedKey = sharedKeyFor(senderRealPublicKey, opts.receiverRealSecretKey);
|
|
156
157
|
const inner = nacl.secretbox.open(cipher, nonce, sharedKey);
|
|
157
158
|
if (!inner || inner.length !== HANDSHAKE_INNER_LENGTH) {
|
|
158
159
|
return undefined;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import nacl from "tweetnacl";
|
|
2
|
+
import { bytesToHex } from "../utils/bytes.js";
|
|
3
|
+
/**
|
|
4
|
+
* X25519 shared-secret derivation, memoised.
|
|
5
|
+
*
|
|
6
|
+
* `nacl.box.before(theirPk, ourSk)` is a scalar multiplication — the single
|
|
7
|
+
* most expensive thing tweetnacl does, and it is pure JS. Every packet that
|
|
8
|
+
* derived its key inline paid for one.
|
|
9
|
+
*
|
|
10
|
+
* That cost is invisible until a node has friends. Measured with a 53-friend
|
|
11
|
+
* list against a peer that could reach none of them (the ordinary case: a
|
|
12
|
+
* laptop whose friends are asleep), the connection loop's cookie requests put
|
|
13
|
+
*
|
|
14
|
+
* 60.8% tweetnacl M (bignum multiply)
|
|
15
|
+
* 11.4% crypto_scalarmult
|
|
16
|
+
*
|
|
17
|
+
* of the process on one function — 72% of the CPU, for keys that never change.
|
|
18
|
+
* A browser tab showed the same shape at 100% of a core for 25 hours.
|
|
19
|
+
*
|
|
20
|
+
* The result depends only on (theirPk, ourSk), so it is cached. Keyed on the
|
|
21
|
+
* public key, with the secret compared by IDENTITY rather than content: a peer
|
|
22
|
+
* passes the same Uint8Array every time, and a different keypair recomputes
|
|
23
|
+
* rather than risking a stale hit. Nothing secret is used as a map key.
|
|
24
|
+
*/
|
|
25
|
+
const cache = new Map();
|
|
26
|
+
const MAX_ENTRIES = 1024;
|
|
27
|
+
export function sharedKey(theirPublicKey, ourSecretKey) {
|
|
28
|
+
const id = bytesToHex(theirPublicKey);
|
|
29
|
+
const hit = cache.get(id);
|
|
30
|
+
if (hit && hit.sk === ourSecretKey)
|
|
31
|
+
return hit.key;
|
|
32
|
+
const key = nacl.box.before(theirPublicKey, ourSecretKey);
|
|
33
|
+
if (cache.size >= MAX_ENTRIES) {
|
|
34
|
+
const oldest = cache.keys().next().value;
|
|
35
|
+
if (oldest !== undefined)
|
|
36
|
+
cache.delete(oldest);
|
|
37
|
+
}
|
|
38
|
+
cache.set(id, { sk: ourSecretKey, key });
|
|
39
|
+
return key;
|
|
40
|
+
}
|
|
41
|
+
/** Drop everything — for tests, and for a key rotation that reuses the same
|
|
42
|
+
* Uint8Array instance. */
|
|
43
|
+
export function clearSharedKeyCache() {
|
|
44
|
+
cache.clear();
|
|
45
|
+
}
|
package/dist/peer.js
CHANGED
|
@@ -6960,7 +6960,41 @@ export class Peer {
|
|
|
6960
6960
|
const wrapped = concatBytes([Uint8Array.of(0x69, 0x76, 0x65, 0x67), packet]);
|
|
6961
6961
|
await this.#udp.send(Buffer.from(wrapped), node.host, node.port);
|
|
6962
6962
|
}
|
|
6963
|
+
/** Hand an onion request to the connected relays, which stand in for hop A. */
|
|
6964
|
+
#sendOnionOverRelays(nodeB, nodeC, nodeD, payloadForNodeD) {
|
|
6965
|
+
if (!this.#tcpRelays || this.#tcpRelays.connectedCount() === 0)
|
|
6966
|
+
return;
|
|
6967
|
+
try {
|
|
6968
|
+
const tcpPacket = createOnionRequest0Tcp({
|
|
6969
|
+
nodeBHost: nodeB.host,
|
|
6970
|
+
nodeBPort: nodeB.port,
|
|
6971
|
+
nodeBPublicKey: nodeB.publicKey,
|
|
6972
|
+
nodeCHost: nodeC.host,
|
|
6973
|
+
nodeCPort: nodeC.port,
|
|
6974
|
+
nodeCPublicKey: nodeC.publicKey,
|
|
6975
|
+
nodeDHost: nodeD.host,
|
|
6976
|
+
nodeDPort: nodeD.port,
|
|
6977
|
+
payloadForNodeD
|
|
6978
|
+
});
|
|
6979
|
+
const sent = this.#tcpRelays.sendOnionRequest(tcpPacket);
|
|
6980
|
+
if (sent > 0) {
|
|
6981
|
+
this.#diagTcpOnionSent += 1;
|
|
6982
|
+
this.#debugVerboseLog(`tcp onion request sent via ${sent} relay(s) to ${nodeD.host}:${nodeD.port}`);
|
|
6983
|
+
}
|
|
6984
|
+
}
|
|
6985
|
+
catch {
|
|
6986
|
+
/* malformed hop key — nothing to send */
|
|
6987
|
+
}
|
|
6988
|
+
}
|
|
6963
6989
|
async #sendThroughOnionPath(payloadForNodeD, nodeD, pathOffset = 0, forcedPath) {
|
|
6990
|
+
// Does this host have real UDP? The browser shim binds and drops, and its
|
|
6991
|
+
// socket reports port 0. Building the UDP onion request there is six
|
|
6992
|
+
// scalar multiplications — three ephemeral keypairs and three
|
|
6993
|
+
// Diffie-Hellmans, in pure JS — for a packet that goes nowhere. Measured:
|
|
6994
|
+
// onion construction was 79% of all crypto time, split almost exactly
|
|
6995
|
+
// between the UDP variant and its TCP twin, and a browser tab held a core
|
|
6996
|
+
// at 100% for 25 hours doing it. Decide BEFORE constructing anything.
|
|
6997
|
+
const udpUsable = (this.#udp?.localPort() ?? 0) > 0;
|
|
6964
6998
|
const path = forcedPath === "direct" ? undefined : forcedPath ?? this.#selectOnionPath(nodeD, pathOffset);
|
|
6965
6999
|
if (!path) {
|
|
6966
7000
|
// No 3-hop UDP path. "Direct" then meant a plain UDP send — which on a
|
|
@@ -6971,37 +7005,29 @@ export class Peer {
|
|
|
6971
7005
|
// A RELAYED onion request needs only two hops: the relay is node A. So
|
|
6972
7006
|
// try that before giving up, and only fall through to the UDP send when
|
|
6973
7007
|
// there are no relays or not even two usable hops.
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
nodeDPort: nodeD.port,
|
|
6987
|
-
payloadForNodeD
|
|
6988
|
-
});
|
|
6989
|
-
const sent = this.#tcpRelays.sendOnionRequest(tcpPacket);
|
|
6990
|
-
if (sent > 0) {
|
|
6991
|
-
this.#diagTcpOnionSent += 1;
|
|
6992
|
-
this.#debugLog(`no 3-hop path for ${nodeD.host}:${nodeD.port} — relayed 2-hop onion via ${sent} relay(s) (B=${hops.nodeB.host} C=${hops.nodeC.host})`);
|
|
6993
|
-
return "direct";
|
|
6994
|
-
}
|
|
6995
|
-
}
|
|
6996
|
-
catch {
|
|
6997
|
-
/* malformed hop key — fall through to the UDP attempt */
|
|
6998
|
-
}
|
|
6999
|
-
}
|
|
7008
|
+
const hops = this.#tcpRelays && this.#tcpRelays.connectedCount() > 0
|
|
7009
|
+
? this.#selectTcpOnionHops(nodeD, pathOffset)
|
|
7010
|
+
: undefined;
|
|
7011
|
+
if (hops) {
|
|
7012
|
+
this.#sendOnionOverRelays(hops.nodeB, hops.nodeC, nodeD, payloadForNodeD);
|
|
7013
|
+
return "direct";
|
|
7014
|
+
}
|
|
7015
|
+
if (!udpUsable) {
|
|
7016
|
+
// Nothing can carry it: no relay hops and no UDP. Say so instead of
|
|
7017
|
+
// handing the packet to a socket that drops it silently.
|
|
7018
|
+
this.#debugLog(`no route for ${nodeD.host}:${nodeD.port} — no relay hops and no UDP`);
|
|
7019
|
+
return "direct";
|
|
7000
7020
|
}
|
|
7001
7021
|
this.#debugLog(`no onion path for ${nodeD.host}:${nodeD.port}, sending direct`);
|
|
7002
7022
|
await this.#sendPacket(payloadForNodeD, nodeD);
|
|
7003
7023
|
return "direct";
|
|
7004
7024
|
}
|
|
7025
|
+
if (!udpUsable) {
|
|
7026
|
+
// No UDP: go straight to the relay form, which is the only one that can
|
|
7027
|
+
// actually leave this host.
|
|
7028
|
+
this.#sendOnionOverRelays(path.nodeB, path.nodeC, nodeD, payloadForNodeD);
|
|
7029
|
+
return path;
|
|
7030
|
+
}
|
|
7005
7031
|
this.#debugLog(`sending onion initial via ${path.nodeA.node.host}:${path.nodeA.node.port} to ${nodeD.host}:${nodeD.port}`);
|
|
7006
7032
|
const packet = createOnionRequest0({
|
|
7007
7033
|
nodeAPublicKey: path.nodeA.publicKey,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.154",
|
|
4
4
|
"description": "Pure TypeScript port of Elastos Carrier (toxcore-derived) P2P messaging. DHT, onion routing, TCP relay, FlatBuffers app payloads, Express offline relay. Wire-compatible with iOS Beagle and the Carrier C SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|