@decentnetwork/peer 0.1.0
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 +31 -0
- package/README.md +97 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +245 -0
- package/dist/compat/address.d.ts +13 -0
- package/dist/compat/address.js +69 -0
- package/dist/compat/bootstrap.d.ts +29 -0
- package/dist/compat/bootstrap.js +178 -0
- package/dist/compat/dht.d.ts +3 -0
- package/dist/compat/dht.js +9 -0
- package/dist/compat/express.d.ts +21 -0
- package/dist/compat/express.js +263 -0
- package/dist/compat/friend.d.ts +4 -0
- package/dist/compat/friend.js +12 -0
- package/dist/compat/net-crypto.d.ts +84 -0
- package/dist/compat/net-crypto.js +278 -0
- package/dist/compat/packet.d.ts +55 -0
- package/dist/compat/packet.js +154 -0
- package/dist/compat/session.d.ts +3 -0
- package/dist/compat/session.js +7 -0
- package/dist/compat/tcp-relay-pool.d.ts +85 -0
- package/dist/compat/tcp-relay-pool.js +342 -0
- package/dist/compat/tcp-relay.d.ts +96 -0
- package/dist/compat/tcp-relay.js +489 -0
- package/dist/compat/text.d.ts +3 -0
- package/dist/compat/text.js +8 -0
- package/dist/compat/tox-dht-crypto.d.ts +18 -0
- package/dist/compat/tox-dht-crypto.js +69 -0
- package/dist/compat/tox-onion.d.ts +66 -0
- package/dist/compat/tox-onion.js +172 -0
- package/dist/crypto/box.d.ts +1 -0
- package/dist/crypto/box.js +3 -0
- package/dist/crypto/keypair.d.ts +5 -0
- package/dist/crypto/keypair.js +37 -0
- package/dist/crypto/nonce.d.ts +1 -0
- package/dist/crypto/nonce.js +1 -0
- package/dist/crypto/sign.d.ts +1 -0
- package/dist/crypto/sign.js +3 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +6 -0
- package/dist/peer.d.ts +45 -0
- package/dist/peer.js +3425 -0
- package/dist/runtime/errors.d.ts +3 -0
- package/dist/runtime/errors.js +6 -0
- package/dist/runtime/events.d.ts +4 -0
- package/dist/runtime/events.js +1 -0
- package/dist/runtime/lifecycle.d.ts +7 -0
- package/dist/runtime/lifecycle.js +12 -0
- package/dist/store/config.d.ts +2 -0
- package/dist/store/config.js +1 -0
- package/dist/store/friends.d.ts +13 -0
- package/dist/store/friends.js +1 -0
- package/dist/store/state.d.ts +3 -0
- package/dist/store/state.js +1 -0
- package/dist/transport/socket.d.ts +4 -0
- package/dist/transport/socket.js +1 -0
- package/dist/transport/tcp.d.ts +3 -0
- package/dist/transport/tcp.js +5 -0
- package/dist/transport/udp.d.ts +24 -0
- package/dist/transport/udp.js +90 -0
- package/dist/types/bootstrap.d.ts +2 -0
- package/dist/types/bootstrap.js +1 -0
- package/dist/types/dht.d.ts +3 -0
- package/dist/types/dht.js +1 -0
- package/dist/types/friend.d.ts +1 -0
- package/dist/types/friend.js +1 -0
- package/dist/types/message.d.ts +1 -0
- package/dist/types/message.js +1 -0
- package/dist/types/peer.d.ts +51 -0
- package/dist/types/peer.js +1 -0
- package/dist/types/session.d.ts +1 -0
- package/dist/types/session.js +1 -0
- package/dist/utils/base58.d.ts +2 -0
- package/dist/utils/base58.js +51 -0
- package/dist/utils/bytes.d.ts +4 -0
- package/dist/utils/bytes.js +31 -0
- package/docs/INSTALL.md +103 -0
- package/docs/USAGE_GUIDE.md +724 -0
- package/package.json +77 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { KeyPair } from "../crypto/keypair.js";
|
|
2
|
+
import type { NetworkNode } from "../types/peer.js";
|
|
3
|
+
type PullCallbacks = {
|
|
4
|
+
onOfflineFriendRequest: (fromUserId: string, packet: Uint8Array, timestamp: number) => void;
|
|
5
|
+
onOfflineFriendMessage: (fromUserId: string, packet: Uint8Array, timestamp: number) => void;
|
|
6
|
+
};
|
|
7
|
+
export declare class LegacyExpressClient {
|
|
8
|
+
#private;
|
|
9
|
+
constructor(opts: {
|
|
10
|
+
nodes: NetworkNode[];
|
|
11
|
+
selfKeyPair: KeyPair;
|
|
12
|
+
selfUserId: string;
|
|
13
|
+
selfAddress: string;
|
|
14
|
+
callbacks: PullCallbacks;
|
|
15
|
+
});
|
|
16
|
+
hasNodes(): boolean;
|
|
17
|
+
sendOfflineFriendRequest(address: string, hello: Uint8Array): Promise<void>;
|
|
18
|
+
sendOfflineText(friendUserId: string, carrierPacket: Uint8Array): Promise<void>;
|
|
19
|
+
pullOnce(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { ByteBuffer, Encoding } from "flatbuffers";
|
|
2
|
+
import nacl from "tweetnacl";
|
|
3
|
+
import { base58ToBytes, bytesToBase58 } from "../utils/base58.js";
|
|
4
|
+
import { randomBytes } from "../utils/bytes.js";
|
|
5
|
+
const EXPRESS_MAGIC = 0xca6ee595;
|
|
6
|
+
const NONCE_SIZE = 24;
|
|
7
|
+
const HTTP_TIMEOUT_MS = 15000;
|
|
8
|
+
export class LegacyExpressClient {
|
|
9
|
+
#nodes;
|
|
10
|
+
#selfKeyPair;
|
|
11
|
+
#selfUserId;
|
|
12
|
+
#selfAddress;
|
|
13
|
+
#currNode = 0;
|
|
14
|
+
#callbacks;
|
|
15
|
+
#debug = process.env.DECENT_DEBUG === "1";
|
|
16
|
+
constructor(opts) {
|
|
17
|
+
this.#selfKeyPair = opts.selfKeyPair;
|
|
18
|
+
this.#selfUserId = opts.selfUserId;
|
|
19
|
+
this.#selfAddress = opts.selfAddress;
|
|
20
|
+
this.#callbacks = opts.callbacks;
|
|
21
|
+
this.#nodes = opts.nodes
|
|
22
|
+
.filter((node) => typeof node.pk === "string" && node.pk.length > 0)
|
|
23
|
+
.map((node) => {
|
|
24
|
+
const expressPk = base58ToBytes(node.pk);
|
|
25
|
+
if (expressPk.length !== 32) {
|
|
26
|
+
throw new Error(`invalid express public key for ${node.host}:${node.port}`);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
host: node.host,
|
|
30
|
+
port: node.port,
|
|
31
|
+
sharedKey: nacl.box.before(expressPk, this.#selfKeyPair.secretKey)
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
hasNodes() {
|
|
36
|
+
return this.#nodes.length > 0;
|
|
37
|
+
}
|
|
38
|
+
async sendOfflineFriendRequest(address, hello) {
|
|
39
|
+
await this.#postEncrypted(address, hello);
|
|
40
|
+
}
|
|
41
|
+
async sendOfflineText(friendUserId, carrierPacket) {
|
|
42
|
+
const friendPk = base58ToBytes(friendUserId);
|
|
43
|
+
if (friendPk.length !== 32) {
|
|
44
|
+
throw new Error("friend user id must decode to 32-byte public key");
|
|
45
|
+
}
|
|
46
|
+
const friendSharedKey = nacl.box.before(friendPk, this.#selfKeyPair.secretKey);
|
|
47
|
+
const friendEncrypted = encrypt(friendSharedKey, carrierPacket);
|
|
48
|
+
await this.#postEncrypted(friendUserId, friendEncrypted);
|
|
49
|
+
}
|
|
50
|
+
async pullOnce() {
|
|
51
|
+
if (!this.#nodes.length) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
let lastTimestamp = 0;
|
|
55
|
+
const { node, body } = await this.#withAnyNode(async (candidate) => ({
|
|
56
|
+
node: candidate,
|
|
57
|
+
body: await this.#http(candidate, "GET", encodeURIComponent(this.#selfUserId))
|
|
58
|
+
}));
|
|
59
|
+
const messages = parseExpressResponseFrames(body);
|
|
60
|
+
if (messages.length > 0) {
|
|
61
|
+
this.#debugLog(`pullOnce got ${messages.length} offline frame(s) from ${node.host}:${node.port}`);
|
|
62
|
+
}
|
|
63
|
+
let requestCount = 0;
|
|
64
|
+
let messageCount = 0;
|
|
65
|
+
for (const encrypted of messages) {
|
|
66
|
+
const plain = decrypt(node.sharedKey, encrypted);
|
|
67
|
+
if (!plain) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const msg = decodePullMessage(plain);
|
|
71
|
+
if (!msg) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
lastTimestamp = Math.max(lastTimestamp, msg.timestamp);
|
|
75
|
+
if (msg.type === "R") {
|
|
76
|
+
if (!msg.address || msg.address !== this.#selfAddress) {
|
|
77
|
+
this.#debugLog(`drop offline request with unmatched address from ${msg.from}`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
this.#callbacks.onOfflineFriendRequest(msg.from, msg.payload, msg.timestamp);
|
|
81
|
+
requestCount += 1;
|
|
82
|
+
this.#debugLog(`offline friend request from ${msg.from} ts=${msg.timestamp}`);
|
|
83
|
+
}
|
|
84
|
+
else if (msg.type === "M") {
|
|
85
|
+
try {
|
|
86
|
+
const friendPk = base58ToBytes(msg.from);
|
|
87
|
+
if (friendPk.length !== 32) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const friendSharedKey = nacl.box.before(friendPk, this.#selfKeyPair.secretKey);
|
|
91
|
+
const packet = decrypt(friendSharedKey, msg.payload);
|
|
92
|
+
if (!packet) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
this.#callbacks.onOfflineFriendMessage(msg.from, packet, msg.timestamp);
|
|
96
|
+
messageCount += 1;
|
|
97
|
+
this.#debugLog(`offline message from ${msg.from} ts=${msg.timestamp}`);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Skip invalid sender ids.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (lastTimestamp > 0) {
|
|
105
|
+
this.#debugLog(`pull processed: requests=${requestCount} messages=${messageCount}; ack until ts=${lastTimestamp}`);
|
|
106
|
+
await this.#deleteUntil(lastTimestamp).catch(() => { });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async #postEncrypted(to, plainData) {
|
|
110
|
+
await this.#withAnyNode(async (node) => {
|
|
111
|
+
const encrypted = encrypt(node.sharedKey, plainData);
|
|
112
|
+
const path = `${encodeURIComponent(to)}/${encodeURIComponent(this.#selfUserId)}`;
|
|
113
|
+
await this.#http(node, "POST", path, encrypted);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async #deleteUntil(timestamp) {
|
|
117
|
+
const tsBytes = new TextEncoder().encode(String(timestamp));
|
|
118
|
+
await this.#withAnyNode(async (node) => {
|
|
119
|
+
const encrypted = encrypt(node.sharedKey, tsBytes);
|
|
120
|
+
const encoded = bytesToBase58(encrypted);
|
|
121
|
+
const path = `${encodeURIComponent(this.#selfUserId)}?until=${encodeURIComponent(encoded)}`;
|
|
122
|
+
await this.#http(node, "DELETE", path);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async #withAnyNode(fn) {
|
|
126
|
+
if (!this.#nodes.length) {
|
|
127
|
+
throw new Error("no express nodes configured");
|
|
128
|
+
}
|
|
129
|
+
let lastError;
|
|
130
|
+
for (let i = 0; i < this.#nodes.length; i++) {
|
|
131
|
+
const idx = (this.#currNode + i) % this.#nodes.length;
|
|
132
|
+
const node = this.#nodes[idx];
|
|
133
|
+
try {
|
|
134
|
+
const value = await fn(node);
|
|
135
|
+
this.#currNode = idx;
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
lastError = error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
143
|
+
}
|
|
144
|
+
async #http(node, method, path, body) {
|
|
145
|
+
const url = `https://${node.host}:${node.port}/${path}`;
|
|
146
|
+
const controller = new AbortController();
|
|
147
|
+
const timer = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
|
|
148
|
+
try {
|
|
149
|
+
const response = await fetch(url, {
|
|
150
|
+
method,
|
|
151
|
+
body: body ? Buffer.from(body) : undefined,
|
|
152
|
+
headers: body ? { "Content-Type": "application/binary" } : undefined,
|
|
153
|
+
signal: controller.signal
|
|
154
|
+
});
|
|
155
|
+
if ((method === "POST" && response.status !== 201) ||
|
|
156
|
+
(method === "GET" && response.status !== 200) ||
|
|
157
|
+
(method === "DELETE" && response.status !== 205)) {
|
|
158
|
+
throw new Error(`express ${method} ${node.host}:${node.port} failed status=${response.status}`);
|
|
159
|
+
}
|
|
160
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
161
|
+
return bytes;
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
#debugLog(message) {
|
|
168
|
+
if (!this.#debug) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
console.log(`[peer-debug:express] ${message}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function encrypt(key, plain) {
|
|
175
|
+
const nonce = randomBytes(NONCE_SIZE);
|
|
176
|
+
const cipher = nacl.secretbox(plain, nonce, key);
|
|
177
|
+
const out = new Uint8Array(nonce.length + cipher.length);
|
|
178
|
+
out.set(nonce, 0);
|
|
179
|
+
out.set(cipher, nonce.length);
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
function decrypt(key, encrypted) {
|
|
183
|
+
if (encrypted.length < NONCE_SIZE + 16) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
const nonce = encrypted.slice(0, NONCE_SIZE);
|
|
187
|
+
const cipher = encrypted.slice(NONCE_SIZE);
|
|
188
|
+
return nacl.secretbox.open(cipher, nonce, key) ?? undefined;
|
|
189
|
+
}
|
|
190
|
+
function parseExpressResponseFrames(body) {
|
|
191
|
+
const out = [];
|
|
192
|
+
let offset = 0;
|
|
193
|
+
while (offset + 8 <= body.length) {
|
|
194
|
+
const magic = readUint32Be(body, offset);
|
|
195
|
+
const len = readUint32Be(body, offset + 4);
|
|
196
|
+
offset += 8;
|
|
197
|
+
if (magic !== EXPRESS_MAGIC) {
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
if (len === 0 || offset + len > body.length) {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
out.push(body.slice(offset, offset + len));
|
|
204
|
+
offset += len;
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
function readUint32Be(bytes, offset) {
|
|
209
|
+
return ((bytes[offset] << 24) |
|
|
210
|
+
(bytes[offset + 1] << 16) |
|
|
211
|
+
(bytes[offset + 2] << 8) |
|
|
212
|
+
bytes[offset + 3]) >>> 0;
|
|
213
|
+
}
|
|
214
|
+
function decodePullMessage(bytes) {
|
|
215
|
+
const bb = new ByteBuffer(bytes);
|
|
216
|
+
const root = bb.readInt32(bb.position()) + bb.position();
|
|
217
|
+
const table = { bb, bb_pos: root };
|
|
218
|
+
const from = readStringField(table, 1) ?? "";
|
|
219
|
+
const typeNum = readUint8Field(table, 2);
|
|
220
|
+
const timestamp = Number(readUint64Field(table, 3));
|
|
221
|
+
const payload = readByteVectorField(table, 5) ?? new Uint8Array();
|
|
222
|
+
const address = readStringField(table, 4);
|
|
223
|
+
if (!from || !Number.isFinite(timestamp)) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
from,
|
|
228
|
+
type: String.fromCharCode(typeNum),
|
|
229
|
+
timestamp,
|
|
230
|
+
address,
|
|
231
|
+
payload
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function readUint8Field(table, field) {
|
|
235
|
+
const offset = table.bb.__offset(table.bb_pos, 4 + field * 2);
|
|
236
|
+
return offset ? table.bb.readUint8(table.bb_pos + offset) : 0;
|
|
237
|
+
}
|
|
238
|
+
function readUint64Field(table, field) {
|
|
239
|
+
const offset = table.bb.__offset(table.bb_pos, 4 + field * 2);
|
|
240
|
+
if (!offset) {
|
|
241
|
+
return 0n;
|
|
242
|
+
}
|
|
243
|
+
const pos = table.bb_pos + offset;
|
|
244
|
+
const low = BigInt(table.bb.readUint32(pos));
|
|
245
|
+
const high = BigInt(table.bb.readUint32(pos + 4));
|
|
246
|
+
return (high << 32n) | low;
|
|
247
|
+
}
|
|
248
|
+
function readStringField(table, field) {
|
|
249
|
+
const offset = table.bb.__offset(table.bb_pos, 4 + field * 2);
|
|
250
|
+
if (!offset) {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
return table.bb.__string(table.bb_pos + offset, Encoding.UTF16_STRING);
|
|
254
|
+
}
|
|
255
|
+
function readByteVectorField(table, field) {
|
|
256
|
+
const offset = table.bb.__offset(table.bb_pos, 4 + field * 2);
|
|
257
|
+
if (!offset) {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
const vector = table.bb.__vector(table.bb_pos + offset);
|
|
261
|
+
const length = table.bb.__vector_len(table.bb_pos + offset);
|
|
262
|
+
return table.bb.bytes().slice(vector, vector + length);
|
|
263
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { LegacyProtocolNotImplementedError } from "../runtime/errors.js";
|
|
2
|
+
export class LegacyFriendClient {
|
|
3
|
+
async sendRequest(pubkey, hello) {
|
|
4
|
+
void pubkey;
|
|
5
|
+
void hello;
|
|
6
|
+
throw new LegacyProtocolNotImplementedError("Legacy friend request");
|
|
7
|
+
}
|
|
8
|
+
async accept(pubkey) {
|
|
9
|
+
void pubkey;
|
|
10
|
+
throw new LegacyProtocolNotImplementedError("Legacy friend accept");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export declare const NET_PACKET_COOKIE_REQUEST = 24;
|
|
2
|
+
export declare const NET_PACKET_COOKIE_RESPONSE = 25;
|
|
3
|
+
export declare const NET_PACKET_CRYPTO_HS = 26;
|
|
4
|
+
export declare const NET_PACKET_CRYPTO_DATA = 27;
|
|
5
|
+
export type CookieRequest = {
|
|
6
|
+
senderDhtPublicKey: Uint8Array;
|
|
7
|
+
senderRealPublicKey: Uint8Array;
|
|
8
|
+
echo: bigint;
|
|
9
|
+
};
|
|
10
|
+
export type CookieResponse = {
|
|
11
|
+
cookie: Uint8Array;
|
|
12
|
+
echo: bigint;
|
|
13
|
+
};
|
|
14
|
+
export type CryptoHandshake = {
|
|
15
|
+
recipientCookie: Uint8Array;
|
|
16
|
+
baseNonce: Uint8Array;
|
|
17
|
+
sessionPublicKey: Uint8Array;
|
|
18
|
+
cookieHash: Uint8Array;
|
|
19
|
+
embeddedCookie: Uint8Array;
|
|
20
|
+
senderRealPublicKey: Uint8Array;
|
|
21
|
+
senderDhtPublicKey: Uint8Array;
|
|
22
|
+
};
|
|
23
|
+
export declare function createCookieRequest(opts: {
|
|
24
|
+
senderRealPublicKey: Uint8Array;
|
|
25
|
+
senderDhtPublicKey: Uint8Array;
|
|
26
|
+
senderDhtSecretKey: Uint8Array;
|
|
27
|
+
receiverDhtPublicKey: Uint8Array;
|
|
28
|
+
echo: bigint;
|
|
29
|
+
}): Uint8Array;
|
|
30
|
+
export declare function openCookieRequest(packet: Uint8Array, opts: {
|
|
31
|
+
receiverDhtSecretKey: Uint8Array;
|
|
32
|
+
}): CookieRequest | undefined;
|
|
33
|
+
export declare function createCookieResponse(opts: {
|
|
34
|
+
request: CookieRequest;
|
|
35
|
+
receiverDhtSecretKey: Uint8Array;
|
|
36
|
+
receiverCookieSymmetricKey: Uint8Array;
|
|
37
|
+
}): Uint8Array;
|
|
38
|
+
export declare function openCookieResponse(packet: Uint8Array, opts: {
|
|
39
|
+
receiverDhtPublicKey: Uint8Array;
|
|
40
|
+
senderDhtSecretKey: Uint8Array;
|
|
41
|
+
}): CookieResponse | undefined;
|
|
42
|
+
export declare function openCookie(cookie: Uint8Array, opts: {
|
|
43
|
+
symmetricKey: Uint8Array;
|
|
44
|
+
}): {
|
|
45
|
+
timestamp: bigint;
|
|
46
|
+
realPublicKey: Uint8Array;
|
|
47
|
+
dhtPublicKey: Uint8Array;
|
|
48
|
+
} | undefined;
|
|
49
|
+
export declare function createCryptoHandshake(opts: {
|
|
50
|
+
recipientCookie: Uint8Array;
|
|
51
|
+
baseNonce: Uint8Array;
|
|
52
|
+
sessionPublicKey: Uint8Array;
|
|
53
|
+
senderRealSecretKey: Uint8Array;
|
|
54
|
+
senderRealPublicKey: Uint8Array;
|
|
55
|
+
senderDhtPublicKey: Uint8Array;
|
|
56
|
+
receiverRealPublicKey: Uint8Array;
|
|
57
|
+
/**
|
|
58
|
+
* Peer's DHT public key. Falls back to receiverRealPublicKey when omitted
|
|
59
|
+
* (Carrier C SDK uses the same key for real and DHT identity).
|
|
60
|
+
*/
|
|
61
|
+
receiverDhtPublicKey?: Uint8Array;
|
|
62
|
+
localCookieSymmetricKey: Uint8Array;
|
|
63
|
+
}): Uint8Array;
|
|
64
|
+
export declare function openCryptoHandshake(packet: Uint8Array, opts: {
|
|
65
|
+
receiverRealSecretKey: Uint8Array;
|
|
66
|
+
receiverCookieSymmetricKey: Uint8Array;
|
|
67
|
+
}): CryptoHandshake | undefined;
|
|
68
|
+
export declare function createCryptoDataPacket(opts: {
|
|
69
|
+
sessionSharedKey: Uint8Array;
|
|
70
|
+
sentNonce: Uint8Array;
|
|
71
|
+
bufferStart: number;
|
|
72
|
+
packetNumber: number;
|
|
73
|
+
payload: Uint8Array;
|
|
74
|
+
}): Uint8Array;
|
|
75
|
+
export declare function openCryptoDataPacket(packet: Uint8Array, opts: {
|
|
76
|
+
sessionSharedKey: Uint8Array;
|
|
77
|
+
recvBaseNonce: Uint8Array;
|
|
78
|
+
}): {
|
|
79
|
+
payload: Uint8Array;
|
|
80
|
+
bufferStart: number;
|
|
81
|
+
packetNumber: number;
|
|
82
|
+
nonceLast2: number;
|
|
83
|
+
} | undefined;
|
|
84
|
+
export declare function incrementNonce(nonce: Uint8Array): void;
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import nacl from "tweetnacl";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { concatBytes, randomBytes } from "../utils/bytes.js";
|
|
4
|
+
export const NET_PACKET_COOKIE_REQUEST = 0x18;
|
|
5
|
+
export const NET_PACKET_COOKIE_RESPONSE = 0x19;
|
|
6
|
+
export const NET_PACKET_CRYPTO_HS = 0x1a;
|
|
7
|
+
export const NET_PACKET_CRYPTO_DATA = 0x1b;
|
|
8
|
+
const KEY_SIZE = 32;
|
|
9
|
+
const NONCE_SIZE = 24;
|
|
10
|
+
const MAC_SIZE = 16;
|
|
11
|
+
const COOKIE_DATA_LENGTH = 64;
|
|
12
|
+
const COOKIE_CONTENTS_LENGTH = 8 + COOKIE_DATA_LENGTH;
|
|
13
|
+
const COOKIE_LENGTH = NONCE_SIZE + COOKIE_CONTENTS_LENGTH + MAC_SIZE; // 112
|
|
14
|
+
const COOKIE_REQUEST_PLAIN_LENGTH = COOKIE_DATA_LENGTH + 8; // 72
|
|
15
|
+
const COOKIE_REQUEST_LENGTH = 1 + KEY_SIZE + NONCE_SIZE + COOKIE_REQUEST_PLAIN_LENGTH + MAC_SIZE; // 145
|
|
16
|
+
const COOKIE_RESPONSE_PLAIN_LENGTH = COOKIE_LENGTH + 8; // 120
|
|
17
|
+
const COOKIE_RESPONSE_LENGTH = 1 + NONCE_SIZE + COOKIE_RESPONSE_PLAIN_LENGTH + MAC_SIZE; // 161
|
|
18
|
+
const HANDSHAKE_INNER_LENGTH = 24 + 32 + 64 + COOKIE_LENGTH; // 232
|
|
19
|
+
const HANDSHAKE_LENGTH = 1 + COOKIE_LENGTH + NONCE_SIZE + HANDSHAKE_INNER_LENGTH + MAC_SIZE; // 385
|
|
20
|
+
const MAX_CRYPTO_PACKET_SIZE = 1400;
|
|
21
|
+
const CRYPTO_DATA_PACKET_MIN_SIZE = 1 + 2 + 4 + 4 + MAC_SIZE;
|
|
22
|
+
const MAX_CRYPTO_DATA_SIZE = MAX_CRYPTO_PACKET_SIZE - CRYPTO_DATA_PACKET_MIN_SIZE;
|
|
23
|
+
const CRYPTO_MAX_PADDING = 8;
|
|
24
|
+
const CRYPTO_DATA_PLAIN_HEADER_LENGTH = 8;
|
|
25
|
+
export function createCookieRequest(opts) {
|
|
26
|
+
if (opts.senderRealPublicKey.length !== KEY_SIZE) {
|
|
27
|
+
throw new Error("sender real public key must be 32 bytes");
|
|
28
|
+
}
|
|
29
|
+
if (opts.senderDhtPublicKey.length !== KEY_SIZE) {
|
|
30
|
+
throw new Error("sender dht public key must be 32 bytes");
|
|
31
|
+
}
|
|
32
|
+
if (opts.senderDhtSecretKey.length !== KEY_SIZE) {
|
|
33
|
+
throw new Error("sender dht secret key must be 32 bytes");
|
|
34
|
+
}
|
|
35
|
+
if (opts.receiverDhtPublicKey.length !== KEY_SIZE) {
|
|
36
|
+
throw new Error("receiver dht public key must be 32 bytes");
|
|
37
|
+
}
|
|
38
|
+
const plain = concatBytes([
|
|
39
|
+
opts.senderRealPublicKey,
|
|
40
|
+
opts.senderDhtPublicKey,
|
|
41
|
+
writeUint64Le(opts.echo)
|
|
42
|
+
]);
|
|
43
|
+
if (plain.length !== COOKIE_REQUEST_PLAIN_LENGTH) {
|
|
44
|
+
throw new Error("cookie request plaintext size mismatch");
|
|
45
|
+
}
|
|
46
|
+
const nonce = randomBytes(NONCE_SIZE);
|
|
47
|
+
const sharedKey = nacl.box.before(opts.receiverDhtPublicKey, opts.senderDhtSecretKey);
|
|
48
|
+
const cipher = nacl.secretbox(plain, nonce, sharedKey);
|
|
49
|
+
return concatBytes([
|
|
50
|
+
Uint8Array.of(NET_PACKET_COOKIE_REQUEST),
|
|
51
|
+
opts.senderDhtPublicKey,
|
|
52
|
+
nonce,
|
|
53
|
+
cipher
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
export function openCookieRequest(packet, opts) {
|
|
57
|
+
if (packet.length !== COOKIE_REQUEST_LENGTH || packet[0] !== NET_PACKET_COOKIE_REQUEST) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const senderDhtPublicKey = packet.slice(1, 1 + KEY_SIZE);
|
|
61
|
+
const nonce = packet.slice(1 + KEY_SIZE, 1 + KEY_SIZE + NONCE_SIZE);
|
|
62
|
+
const cipher = packet.slice(1 + KEY_SIZE + NONCE_SIZE);
|
|
63
|
+
const sharedKey = nacl.box.before(senderDhtPublicKey, opts.receiverDhtSecretKey);
|
|
64
|
+
const plain = nacl.secretbox.open(cipher, nonce, sharedKey);
|
|
65
|
+
if (!plain || plain.length !== COOKIE_REQUEST_PLAIN_LENGTH) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
senderDhtPublicKey,
|
|
70
|
+
senderRealPublicKey: plain.slice(0, KEY_SIZE),
|
|
71
|
+
echo: readUint64Le(plain, COOKIE_DATA_LENGTH)
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function createCookieResponse(opts) {
|
|
75
|
+
const cookie = createCookie({
|
|
76
|
+
symmetricKey: opts.receiverCookieSymmetricKey,
|
|
77
|
+
realPublicKey: opts.request.senderRealPublicKey,
|
|
78
|
+
dhtPublicKey: opts.request.senderDhtPublicKey
|
|
79
|
+
});
|
|
80
|
+
const plain = concatBytes([cookie, writeUint64Le(opts.request.echo)]);
|
|
81
|
+
const nonce = randomBytes(NONCE_SIZE);
|
|
82
|
+
const sharedKey = nacl.box.before(opts.request.senderDhtPublicKey, opts.receiverDhtSecretKey);
|
|
83
|
+
const cipher = nacl.secretbox(plain, nonce, sharedKey);
|
|
84
|
+
return concatBytes([Uint8Array.of(NET_PACKET_COOKIE_RESPONSE), nonce, cipher]);
|
|
85
|
+
}
|
|
86
|
+
export function openCookieResponse(packet, opts) {
|
|
87
|
+
if (packet.length !== COOKIE_RESPONSE_LENGTH || packet[0] !== NET_PACKET_COOKIE_RESPONSE) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
const nonce = packet.slice(1, 1 + NONCE_SIZE);
|
|
91
|
+
const cipher = packet.slice(1 + NONCE_SIZE);
|
|
92
|
+
const sharedKey = nacl.box.before(opts.receiverDhtPublicKey, opts.senderDhtSecretKey);
|
|
93
|
+
const plain = nacl.secretbox.open(cipher, nonce, sharedKey);
|
|
94
|
+
if (!plain || plain.length !== COOKIE_RESPONSE_PLAIN_LENGTH) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
cookie: plain.slice(0, COOKIE_LENGTH),
|
|
99
|
+
echo: readUint64Le(plain, COOKIE_LENGTH)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function openCookie(cookie, opts) {
|
|
103
|
+
if (cookie.length !== COOKIE_LENGTH) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const nonce = cookie.slice(0, NONCE_SIZE);
|
|
107
|
+
const cipher = cookie.slice(NONCE_SIZE);
|
|
108
|
+
const plain = nacl.secretbox.open(cipher, nonce, opts.symmetricKey);
|
|
109
|
+
if (!plain || plain.length !== COOKIE_CONTENTS_LENGTH) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
timestamp: readUint64Le(plain, 0),
|
|
114
|
+
realPublicKey: plain.slice(8, 8 + KEY_SIZE),
|
|
115
|
+
dhtPublicKey: plain.slice(8 + KEY_SIZE, 8 + KEY_SIZE + KEY_SIZE)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function createCryptoHandshake(opts) {
|
|
119
|
+
const innerNonce = randomBytes(NONCE_SIZE);
|
|
120
|
+
// Toxcore create_crypto_handshake: the embedded "own cookie" is encrypted
|
|
121
|
+
// for the *peer's* future use and contains the PEER's real_pk + dht_pk.
|
|
122
|
+
// The peer attaches this cookie at handshake[1..113] when they reply, and
|
|
123
|
+
// we decrypt it with our symmetric key to recover their keys for inner box.
|
|
124
|
+
// Earlier versions of this code mistakenly stored the SENDER's own keys in
|
|
125
|
+
// ownCookie, which made the responder's reply handshake fail to decrypt
|
|
126
|
+
// because we recovered our own pubkey instead of the peer's.
|
|
127
|
+
const ownCookie = createCookie({
|
|
128
|
+
symmetricKey: opts.localCookieSymmetricKey,
|
|
129
|
+
realPublicKey: opts.receiverRealPublicKey,
|
|
130
|
+
dhtPublicKey: opts.receiverDhtPublicKey ?? opts.receiverRealPublicKey
|
|
131
|
+
});
|
|
132
|
+
const innerPlain = concatBytes([
|
|
133
|
+
opts.baseNonce,
|
|
134
|
+
opts.sessionPublicKey,
|
|
135
|
+
sha512(opts.recipientCookie),
|
|
136
|
+
ownCookie
|
|
137
|
+
]);
|
|
138
|
+
const sharedKey = nacl.box.before(opts.receiverRealPublicKey, opts.senderRealSecretKey);
|
|
139
|
+
const innerCipher = nacl.secretbox(innerPlain, innerNonce, sharedKey);
|
|
140
|
+
return concatBytes([Uint8Array.of(NET_PACKET_CRYPTO_HS), opts.recipientCookie, innerNonce, innerCipher]);
|
|
141
|
+
}
|
|
142
|
+
export function openCryptoHandshake(packet, opts) {
|
|
143
|
+
if (packet.length !== HANDSHAKE_LENGTH || packet[0] !== NET_PACKET_CRYPTO_HS) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
const recipientCookie = packet.slice(1, 1 + COOKIE_LENGTH);
|
|
147
|
+
const cookieParsed = openCookie(recipientCookie, { symmetricKey: opts.receiverCookieSymmetricKey });
|
|
148
|
+
if (!cookieParsed) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
const senderRealPublicKey = cookieParsed.realPublicKey;
|
|
152
|
+
const senderDhtPublicKey = cookieParsed.dhtPublicKey;
|
|
153
|
+
const nonce = packet.slice(1 + COOKIE_LENGTH, 1 + COOKIE_LENGTH + NONCE_SIZE);
|
|
154
|
+
const cipher = packet.slice(1 + COOKIE_LENGTH + NONCE_SIZE);
|
|
155
|
+
const sharedKey = nacl.box.before(senderRealPublicKey, opts.receiverRealSecretKey);
|
|
156
|
+
const inner = nacl.secretbox.open(cipher, nonce, sharedKey);
|
|
157
|
+
if (!inner || inner.length !== HANDSHAKE_INNER_LENGTH) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
const baseNonce = inner.slice(0, NONCE_SIZE);
|
|
161
|
+
const sessionPublicKey = inner.slice(NONCE_SIZE, NONCE_SIZE + KEY_SIZE);
|
|
162
|
+
const cookieHash = inner.slice(NONCE_SIZE + KEY_SIZE, NONCE_SIZE + KEY_SIZE + 64);
|
|
163
|
+
if (!bytesEqual(cookieHash, sha512(recipientCookie))) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
const embeddedCookie = inner.slice(NONCE_SIZE + KEY_SIZE + 64);
|
|
167
|
+
return {
|
|
168
|
+
recipientCookie,
|
|
169
|
+
baseNonce,
|
|
170
|
+
sessionPublicKey,
|
|
171
|
+
cookieHash,
|
|
172
|
+
embeddedCookie,
|
|
173
|
+
senderRealPublicKey,
|
|
174
|
+
senderDhtPublicKey
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
export function createCryptoDataPacket(opts) {
|
|
178
|
+
if (opts.payload.length === 0 || opts.payload.length > MAX_CRYPTO_DATA_SIZE) {
|
|
179
|
+
throw new Error(`crypto data payload must be 1..${MAX_CRYPTO_DATA_SIZE} bytes`);
|
|
180
|
+
}
|
|
181
|
+
const paddingLength = (MAX_CRYPTO_DATA_SIZE - opts.payload.length) % CRYPTO_MAX_PADDING;
|
|
182
|
+
const plain = new Uint8Array(CRYPTO_DATA_PLAIN_HEADER_LENGTH + paddingLength + opts.payload.length);
|
|
183
|
+
writeUint32BeInto(plain, 0, opts.bufferStart);
|
|
184
|
+
writeUint32BeInto(plain, 4, opts.packetNumber);
|
|
185
|
+
plain.set(opts.payload, CRYPTO_DATA_PLAIN_HEADER_LENGTH + paddingLength);
|
|
186
|
+
const cipher = nacl.secretbox(plain, opts.sentNonce, opts.sessionSharedKey);
|
|
187
|
+
const last2 = opts.sentNonce.slice(opts.sentNonce.length - 2);
|
|
188
|
+
return concatBytes([Uint8Array.of(NET_PACKET_CRYPTO_DATA), last2, cipher]);
|
|
189
|
+
}
|
|
190
|
+
export function openCryptoDataPacket(packet, opts) {
|
|
191
|
+
if (packet.length <= CRYPTO_DATA_PACKET_MIN_SIZE || packet[0] !== NET_PACKET_CRYPTO_DATA) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
const nonce = opts.recvBaseNonce.slice();
|
|
195
|
+
nonce[nonce.length - 2] = packet[1];
|
|
196
|
+
nonce[nonce.length - 1] = packet[2];
|
|
197
|
+
const cipher = packet.slice(3);
|
|
198
|
+
const plain = nacl.secretbox.open(cipher, nonce, opts.sessionSharedKey);
|
|
199
|
+
if (!plain || plain.length <= CRYPTO_DATA_PLAIN_HEADER_LENGTH) {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
const bufferStart = readUint32Be(plain, 0);
|
|
203
|
+
const packetNumber = readUint32Be(plain, 4);
|
|
204
|
+
let payloadOffset = CRYPTO_DATA_PLAIN_HEADER_LENGTH;
|
|
205
|
+
while (payloadOffset < plain.length && plain[payloadOffset] === 0) {
|
|
206
|
+
payloadOffset += 1;
|
|
207
|
+
}
|
|
208
|
+
if (payloadOffset >= plain.length) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
payload: plain.slice(payloadOffset),
|
|
213
|
+
bufferStart,
|
|
214
|
+
packetNumber,
|
|
215
|
+
nonceLast2: ((packet[1] << 8) | packet[2]) >>> 0
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function createCookie(opts) {
|
|
219
|
+
const timestamp = BigInt(Math.floor(Date.now() / 1000));
|
|
220
|
+
const contents = concatBytes([
|
|
221
|
+
writeUint64Le(timestamp),
|
|
222
|
+
opts.realPublicKey,
|
|
223
|
+
opts.dhtPublicKey
|
|
224
|
+
]);
|
|
225
|
+
const nonce = randomBytes(NONCE_SIZE);
|
|
226
|
+
const cipher = nacl.secretbox(contents, nonce, opts.symmetricKey);
|
|
227
|
+
return concatBytes([nonce, cipher]);
|
|
228
|
+
}
|
|
229
|
+
export function incrementNonce(nonce) {
|
|
230
|
+
for (let i = nonce.length - 1; i >= 0; i--) {
|
|
231
|
+
nonce[i] = (nonce[i] + 1) & 0xff;
|
|
232
|
+
if (nonce[i] !== 0) {
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function sha512(data) {
|
|
238
|
+
return new Uint8Array(createHash("sha512").update(data).digest());
|
|
239
|
+
}
|
|
240
|
+
function bytesEqual(a, b) {
|
|
241
|
+
if (a.length !== b.length) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
let diff = 0;
|
|
245
|
+
for (let i = 0; i < a.length; i++) {
|
|
246
|
+
diff |= a[i] ^ b[i];
|
|
247
|
+
}
|
|
248
|
+
return diff === 0;
|
|
249
|
+
}
|
|
250
|
+
function writeUint64Le(value) {
|
|
251
|
+
const bytes = new Uint8Array(8);
|
|
252
|
+
let v = value;
|
|
253
|
+
for (let i = 0; i < 8; i++) {
|
|
254
|
+
bytes[i] = Number(v & 0xffn);
|
|
255
|
+
v >>= 8n;
|
|
256
|
+
}
|
|
257
|
+
return bytes;
|
|
258
|
+
}
|
|
259
|
+
function readUint64Le(bytes, offset) {
|
|
260
|
+
let value = 0n;
|
|
261
|
+
for (let i = 7; i >= 0; i--) {
|
|
262
|
+
value = (value << 8n) | BigInt(bytes[offset + i]);
|
|
263
|
+
}
|
|
264
|
+
return value;
|
|
265
|
+
}
|
|
266
|
+
function writeUint32BeInto(bytes, offset, value) {
|
|
267
|
+
const v = value >>> 0;
|
|
268
|
+
bytes[offset] = (v >>> 24) & 0xff;
|
|
269
|
+
bytes[offset + 1] = (v >>> 16) & 0xff;
|
|
270
|
+
bytes[offset + 2] = (v >>> 8) & 0xff;
|
|
271
|
+
bytes[offset + 3] = v & 0xff;
|
|
272
|
+
}
|
|
273
|
+
function readUint32Be(bytes, offset) {
|
|
274
|
+
return (((bytes[offset] << 24) >>> 0) |
|
|
275
|
+
(bytes[offset + 1] << 16) |
|
|
276
|
+
(bytes[offset + 2] << 8) |
|
|
277
|
+
bytes[offset + 3]) >>> 0;
|
|
278
|
+
}
|