dignity.js 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 +201 -0
- package/README.md +197 -0
- package/dist/dignity.cjs.js +3585 -0
- package/dist/dignity.cjs.js.map +7 -0
- package/dist/dignity.esm.js +3606 -0
- package/dist/dignity.esm.js.map +7 -0
- package/dist/dignity.min.js +1 -0
- package/docs/assets/dignity-logo.svg +54 -0
- package/docs/assets/styles.css +68 -0
- package/docs/index.html +117 -0
- package/docs/openapi-like.json +64 -0
- package/examples/decentralized-chess-lite.js +102 -0
- package/examples/decentralized-tictactoe.js +112 -0
- package/package.json +53 -0
- package/src/core/dignity-p2p.js +604 -0
- package/src/index.js +41 -0
- package/src/network/in-memory-network.js +77 -0
- package/src/security/message-security-service.js +462 -0
- package/src/security/sloth-vdf.js +83 -0
- package/src/security/vdf.js +23 -0
- package/src/signaling/create-default-signaling-pool.js +44 -0
- package/src/signaling/default-signaling-config.js +15 -0
- package/src/signaling/signaling-pool.js +75 -0
- package/src/signaling/websocket-signaling-provider.js +69 -0
- package/src/utils/event-emitter.js +38 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
const nacl = require('tweetnacl');
|
|
2
|
+
const naclUtil = require('tweetnacl-util');
|
|
3
|
+
const VDF = require('./vdf');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_SECURITY_OPTIONS = {
|
|
6
|
+
enabled: true,
|
|
7
|
+
signingEnabled: true,
|
|
8
|
+
encryptionEnabled: true,
|
|
9
|
+
powEnabled: true,
|
|
10
|
+
powTargetMs: 1000,
|
|
11
|
+
appPassword: 'change-this-app-password',
|
|
12
|
+
broadcastPasswords: {},
|
|
13
|
+
resolveBroadcastPassword: null,
|
|
14
|
+
powSteps: 22,
|
|
15
|
+
trustedPeerKeys: {}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function stableStringify(value) {
|
|
19
|
+
if (value === null || typeof value !== 'object') {
|
|
20
|
+
return JSON.stringify(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
return `[${value.map((entry) => stableStringify(entry)).join(',')}]`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const keys = Object.keys(value).sort();
|
|
28
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function concatBytes(a, b) {
|
|
32
|
+
const result = new Uint8Array(a.length + b.length);
|
|
33
|
+
result.set(a, 0);
|
|
34
|
+
result.set(b, a.length);
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function hash32(bytes) {
|
|
39
|
+
return nacl.hash(bytes).slice(0, 32);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function bytesToHex(bytes) {
|
|
43
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function utf8ToBytes(value) {
|
|
47
|
+
return naclUtil.decodeUTF8(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizePeerPublicKey(publicKey) {
|
|
51
|
+
if (!publicKey || typeof publicKey !== 'object') {
|
|
52
|
+
throw new Error('Public key must be an object with signingPublicKey and encryptionPublicKey');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!publicKey.signingPublicKey || !publicKey.encryptionPublicKey) {
|
|
56
|
+
throw new Error('Public key object is missing signingPublicKey or encryptionPublicKey');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
signingPublicKey: publicKey.signingPublicKey,
|
|
61
|
+
encryptionPublicKey: publicKey.encryptionPublicKey
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class MessageSecurityService {
|
|
66
|
+
constructor({ nodeId, options = {}, now } = {}) {
|
|
67
|
+
if (!nodeId) {
|
|
68
|
+
throw new Error('MessageSecurityService requires nodeId');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.nodeId = nodeId;
|
|
72
|
+
this.options = {
|
|
73
|
+
...DEFAULT_SECURITY_OPTIONS,
|
|
74
|
+
...options
|
|
75
|
+
};
|
|
76
|
+
this.now = now || (() => Date.now());
|
|
77
|
+
|
|
78
|
+
const keyPair = options.keyPair || {
|
|
79
|
+
signing: nacl.sign.keyPair(),
|
|
80
|
+
encryption: nacl.box.keyPair()
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
this.signingSecretKey = keyPair.signing.secretKey;
|
|
84
|
+
this.signingPublicKey = keyPair.signing.publicKey;
|
|
85
|
+
this.encryptionSecretKey = keyPair.encryption.secretKey;
|
|
86
|
+
this.encryptionPublicKey = keyPair.encryption.publicKey;
|
|
87
|
+
|
|
88
|
+
this.publicKeyBundle = {
|
|
89
|
+
signingPublicKey: naclUtil.encodeBase64(this.signingPublicKey),
|
|
90
|
+
encryptionPublicKey: naclUtil.encodeBase64(this.encryptionPublicKey)
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
this.peerPublicKeys = new Map();
|
|
94
|
+
for (const [peerId, peerKey] of Object.entries(this.options.trustedPeerKeys || {})) {
|
|
95
|
+
this.peerPublicKeys.set(peerId, normalizePeerPublicKey(peerKey));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.calibratedPowSteps = this.options.powSteps;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getPublicKey() {
|
|
102
|
+
return { ...this.publicKeyBundle };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
registerPeerPublicKey(peerId, publicKey) {
|
|
106
|
+
this.peerPublicKeys.set(peerId, normalizePeerPublicKey(publicKey));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
resolvePeerPublicKey(peerId, fallbackPublicKey) {
|
|
110
|
+
const trusted = this.peerPublicKeys.get(peerId);
|
|
111
|
+
const fallback = fallbackPublicKey ? normalizePeerPublicKey(fallbackPublicKey) : null;
|
|
112
|
+
|
|
113
|
+
if (trusted && fallback) {
|
|
114
|
+
const mismatch =
|
|
115
|
+
trusted.signingPublicKey !== fallback.signingPublicKey ||
|
|
116
|
+
trusted.encryptionPublicKey !== fallback.encryptionPublicKey;
|
|
117
|
+
|
|
118
|
+
if (mismatch) {
|
|
119
|
+
throw new Error(`Public key mismatch for peer ${peerId}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return trusted || fallback || null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
buildEnvelopeBase({ messageType, payload, targetId = null }) {
|
|
127
|
+
return {
|
|
128
|
+
version: 1,
|
|
129
|
+
senderId: this.nodeId,
|
|
130
|
+
senderPublicKey: this.getPublicKey(),
|
|
131
|
+
targetId,
|
|
132
|
+
messageType,
|
|
133
|
+
timestamp: this.now(),
|
|
134
|
+
payload
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async secureOutgoingMessage({ messageType, payload, targetId = null, securityContext = {} }) {
|
|
139
|
+
if (!this.options.enabled) {
|
|
140
|
+
return this.buildEnvelopeBase({ messageType, payload, targetId });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const envelope = this.buildEnvelopeBase({ messageType, payload, targetId });
|
|
144
|
+
const encryptionInfo = await this.encryptPayload({ payload, targetId, securityContext });
|
|
145
|
+
envelope.payload = encryptionInfo.payload;
|
|
146
|
+
envelope.security = {
|
|
147
|
+
encryption: encryptionInfo.security,
|
|
148
|
+
signing: { enabled: false },
|
|
149
|
+
pow: { enabled: false }
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (this.options.powEnabled) {
|
|
153
|
+
const pow = await this.generatePow(envelope);
|
|
154
|
+
envelope.security.pow = {
|
|
155
|
+
enabled: true,
|
|
156
|
+
challenge: pow.challenge,
|
|
157
|
+
proof: pow.proof,
|
|
158
|
+
steps: pow.steps,
|
|
159
|
+
durationMs: pow.durationMs
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (this.options.signingEnabled) {
|
|
164
|
+
const signatureBase = this.canonicalSigningInput(envelope);
|
|
165
|
+
const signature = nacl.sign.detached(
|
|
166
|
+
naclUtil.decodeUTF8(signatureBase),
|
|
167
|
+
this.signingSecretKey
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
envelope.security.signing = {
|
|
171
|
+
enabled: true,
|
|
172
|
+
algorithm: 'ed25519',
|
|
173
|
+
signature: naclUtil.encodeBase64(signature)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return envelope;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
canonicalSigningInput(envelope) {
|
|
181
|
+
return stableStringify({
|
|
182
|
+
version: envelope.version,
|
|
183
|
+
senderId: envelope.senderId,
|
|
184
|
+
senderPublicKey: envelope.senderPublicKey,
|
|
185
|
+
targetId: envelope.targetId,
|
|
186
|
+
messageType: envelope.messageType,
|
|
187
|
+
timestamp: envelope.timestamp,
|
|
188
|
+
payload: envelope.payload,
|
|
189
|
+
security: {
|
|
190
|
+
encryption: envelope.security ? envelope.security.encryption : { enabled: false },
|
|
191
|
+
pow: envelope.security ? envelope.security.pow : { enabled: false }
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
canonicalPowInput(envelope) {
|
|
197
|
+
return stableStringify({
|
|
198
|
+
version: envelope.version,
|
|
199
|
+
senderId: envelope.senderId,
|
|
200
|
+
senderPublicKey: envelope.senderPublicKey,
|
|
201
|
+
targetId: envelope.targetId,
|
|
202
|
+
messageType: envelope.messageType,
|
|
203
|
+
timestamp: envelope.timestamp,
|
|
204
|
+
payload: envelope.payload,
|
|
205
|
+
security: {
|
|
206
|
+
encryption: envelope.security ? envelope.security.encryption : { enabled: false }
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async decryptIncomingMessage(envelope) {
|
|
212
|
+
if (!this.options.enabled) {
|
|
213
|
+
return {
|
|
214
|
+
ignored: false,
|
|
215
|
+
messageType: envelope.messageType,
|
|
216
|
+
senderId: envelope.senderId,
|
|
217
|
+
targetId: envelope.targetId,
|
|
218
|
+
payload: envelope.payload
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!envelope || typeof envelope !== 'object') {
|
|
223
|
+
throw new Error('Incoming message is invalid');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (envelope.targetId && envelope.targetId !== this.nodeId) {
|
|
227
|
+
return { ignored: true };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (envelope.security && envelope.security.pow && envelope.security.pow.enabled && this.options.powEnabled) {
|
|
231
|
+
await this.verifyPow(envelope);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (envelope.security && envelope.security.signing && envelope.security.signing.enabled && this.options.signingEnabled) {
|
|
235
|
+
this.verifySignature(envelope);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const payload = this.decryptPayload(envelope);
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
ignored: false,
|
|
242
|
+
messageType: envelope.messageType,
|
|
243
|
+
senderId: envelope.senderId,
|
|
244
|
+
targetId: envelope.targetId,
|
|
245
|
+
payload
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
resolveBroadcastPassword(scope) {
|
|
250
|
+
const normalizedScope = scope || 'default';
|
|
251
|
+
|
|
252
|
+
if (typeof this.options.resolveBroadcastPassword === 'function') {
|
|
253
|
+
const resolved = this.options.resolveBroadcastPassword({
|
|
254
|
+
scope: normalizedScope,
|
|
255
|
+
nodeId: this.nodeId,
|
|
256
|
+
defaultPassword: this.options.appPassword,
|
|
257
|
+
broadcastPasswords: this.options.broadcastPasswords || {}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (typeof resolved === 'string' && resolved.length > 0) {
|
|
261
|
+
return resolved;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const scopePassword = this.options.broadcastPasswords
|
|
266
|
+
? this.options.broadcastPasswords[normalizedScope]
|
|
267
|
+
: null;
|
|
268
|
+
|
|
269
|
+
if (typeof scopePassword === 'string' && scopePassword.length > 0) {
|
|
270
|
+
return scopePassword;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return this.options.appPassword;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async encryptPayload({ payload, targetId, securityContext = {} }) {
|
|
277
|
+
if (!this.options.encryptionEnabled) {
|
|
278
|
+
return {
|
|
279
|
+
payload,
|
|
280
|
+
security: {
|
|
281
|
+
enabled: false,
|
|
282
|
+
mode: 'none'
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const plainText = naclUtil.decodeUTF8(JSON.stringify(payload));
|
|
288
|
+
|
|
289
|
+
if (targetId) {
|
|
290
|
+
const recipientPublicKey = this.resolvePeerPublicKey(targetId, null);
|
|
291
|
+
if (!recipientPublicKey) {
|
|
292
|
+
throw new Error(`Missing public key for target peer ${targetId}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const nonce = nacl.randomBytes(nacl.box.nonceLength);
|
|
296
|
+
const encrypted = nacl.box(
|
|
297
|
+
plainText,
|
|
298
|
+
nonce,
|
|
299
|
+
naclUtil.decodeBase64(recipientPublicKey.encryptionPublicKey),
|
|
300
|
+
this.encryptionSecretKey
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
payload: naclUtil.encodeBase64(encrypted),
|
|
305
|
+
security: {
|
|
306
|
+
enabled: true,
|
|
307
|
+
mode: 'direct',
|
|
308
|
+
nonce: naclUtil.encodeBase64(nonce),
|
|
309
|
+
senderEncryptionPublicKey: this.publicKeyBundle.encryptionPublicKey
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const scope = securityContext.broadcastScope || 'default';
|
|
315
|
+
const nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
|
|
316
|
+
const salt = nacl.randomBytes(16);
|
|
317
|
+
const password = this.resolveBroadcastPassword(scope);
|
|
318
|
+
const key = hash32(concatBytes(utf8ToBytes(password), salt));
|
|
319
|
+
const encrypted = nacl.secretbox(plainText, nonce, key);
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
payload: naclUtil.encodeBase64(encrypted),
|
|
323
|
+
security: {
|
|
324
|
+
enabled: true,
|
|
325
|
+
mode: 'broadcast',
|
|
326
|
+
scope,
|
|
327
|
+
nonce: naclUtil.encodeBase64(nonce),
|
|
328
|
+
salt: naclUtil.encodeBase64(salt)
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
decryptPayload(envelope) {
|
|
334
|
+
const encryption = envelope.security ? envelope.security.encryption : null;
|
|
335
|
+
|
|
336
|
+
if (!encryption || !encryption.enabled || !this.options.encryptionEnabled) {
|
|
337
|
+
return envelope.payload;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const encryptedBuffer = naclUtil.decodeBase64(envelope.payload);
|
|
341
|
+
|
|
342
|
+
if (encryption.mode === 'broadcast') {
|
|
343
|
+
const scope = encryption.scope || 'default';
|
|
344
|
+
const password = this.resolveBroadcastPassword(scope);
|
|
345
|
+
const salt = naclUtil.decodeBase64(encryption.salt);
|
|
346
|
+
const nonce = naclUtil.decodeBase64(encryption.nonce);
|
|
347
|
+
const key = hash32(concatBytes(utf8ToBytes(password), salt));
|
|
348
|
+
const decrypted = nacl.secretbox.open(encryptedBuffer, nonce, key);
|
|
349
|
+
|
|
350
|
+
if (!decrypted) {
|
|
351
|
+
throw new Error('Unable to decrypt broadcast payload');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return JSON.parse(naclUtil.encodeUTF8(decrypted));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (encryption.mode === 'direct') {
|
|
358
|
+
const senderPublicKey = naclUtil.decodeBase64(encryption.senderEncryptionPublicKey);
|
|
359
|
+
const nonce = naclUtil.decodeBase64(encryption.nonce);
|
|
360
|
+
const decrypted = nacl.box.open(
|
|
361
|
+
encryptedBuffer,
|
|
362
|
+
nonce,
|
|
363
|
+
senderPublicKey,
|
|
364
|
+
this.encryptionSecretKey
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
if (!decrypted) {
|
|
368
|
+
throw new Error('Unable to decrypt direct payload');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return JSON.parse(naclUtil.encodeUTF8(decrypted));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
throw new Error(`Unsupported encryption mode: ${encryption.mode}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
verifySignature(envelope) {
|
|
378
|
+
const senderPublicKey = this.resolvePeerPublicKey(envelope.senderId, envelope.senderPublicKey);
|
|
379
|
+
if (!senderPublicKey) {
|
|
380
|
+
throw new Error(`Missing public key for sender ${envelope.senderId}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const signatureBase = this.canonicalSigningInput(envelope);
|
|
384
|
+
const isValid = nacl.sign.detached.verify(
|
|
385
|
+
naclUtil.decodeUTF8(signatureBase),
|
|
386
|
+
naclUtil.decodeBase64(envelope.security.signing.signature),
|
|
387
|
+
naclUtil.decodeBase64(senderPublicKey.signingPublicKey)
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
if (!isValid) {
|
|
391
|
+
const error = new Error(`Invalid signature for sender ${envelope.senderId}`);
|
|
392
|
+
error.code = 'INVALID_SIGNATURE';
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async determinePowSteps() {
|
|
400
|
+
if (typeof this.calibratedPowSteps === 'bigint') {
|
|
401
|
+
return this.calibratedPowSteps;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (typeof this.options.powSteps === 'number') {
|
|
405
|
+
this.calibratedPowSteps = BigInt(Math.max(1, this.options.powSteps));
|
|
406
|
+
return this.calibratedPowSteps;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const targetMs = Math.max(1, Number(this.options.powTargetMs || 1));
|
|
410
|
+
const probeChallenge = bytesToHex(hash32(utf8ToBytes(`probe:${this.nodeId}:${this.now()}`)));
|
|
411
|
+
const probeSteps = BigInt(2);
|
|
412
|
+
|
|
413
|
+
const start = this.now();
|
|
414
|
+
await VDF.compute(probeChallenge, probeSteps);
|
|
415
|
+
const elapsedMs = Math.max(1, this.now() - start);
|
|
416
|
+
|
|
417
|
+
const scaled = Math.max(1, Math.round((targetMs / elapsedMs) * Number(probeSteps)));
|
|
418
|
+
this.calibratedPowSteps = BigInt(scaled);
|
|
419
|
+
return this.calibratedPowSteps;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async generatePow(envelope) {
|
|
423
|
+
const challenge = bytesToHex(hash32(utf8ToBytes(this.canonicalPowInput(envelope))));
|
|
424
|
+
const steps = await this.determinePowSteps();
|
|
425
|
+
const start = this.now();
|
|
426
|
+
const proof = await VDF.compute(challenge, steps);
|
|
427
|
+
const durationMs = this.now() - start;
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
challenge,
|
|
431
|
+
proof,
|
|
432
|
+
steps: steps.toString(),
|
|
433
|
+
durationMs
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async verifyPow(envelope) {
|
|
438
|
+
const expectedChallenge = bytesToHex(hash32(utf8ToBytes(this.canonicalPowInput(envelope))));
|
|
439
|
+
const pow = envelope.security.pow;
|
|
440
|
+
|
|
441
|
+
if (!pow || pow.challenge !== expectedChallenge) {
|
|
442
|
+
const error = new Error('PoW challenge mismatch');
|
|
443
|
+
error.code = 'INVALID_POW';
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const verified = await VDF.verify(pow.challenge, BigInt(pow.steps), pow.proof);
|
|
448
|
+
if (!verified) {
|
|
449
|
+
const error = new Error('PoW verification failed');
|
|
450
|
+
error.code = 'INVALID_POW';
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
module.exports = {
|
|
459
|
+
MessageSecurityService,
|
|
460
|
+
stableStringify,
|
|
461
|
+
DEFAULT_SECURITY_OPTIONS
|
|
462
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Sloth permutation adaptation based on:
|
|
3
|
+
* https://github.com/hyperhyperspace/pulsar/blob/main/src/model/SlothVDF.ts
|
|
4
|
+
*/
|
|
5
|
+
class SlothPermutation {
|
|
6
|
+
static p = BigInt(
|
|
7
|
+
'170082004324204494273811327264862981553264701145937538369570764779791492622392118654022654452947093285873855529044371650895045691292912712699015605832276411308653107069798639938826015099738961427172366594187783204437869906954750443653318078358839409699824714551430573905637228307966826784684174483831608534979'
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
fastPow(base, exponent, modulus) {
|
|
11
|
+
if (modulus === BigInt(1)) {
|
|
12
|
+
return BigInt(0);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let result = BigInt(1);
|
|
16
|
+
let powBase = base % modulus;
|
|
17
|
+
let powExponent = exponent;
|
|
18
|
+
|
|
19
|
+
while (powExponent > 0) {
|
|
20
|
+
if (powExponent % BigInt(2) === BigInt(1)) {
|
|
21
|
+
result = (result * powBase) % modulus;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
powExponent = powExponent / BigInt(2);
|
|
25
|
+
powBase = (powBase * powBase) % modulus;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
quadRes(x) {
|
|
32
|
+
return this.fastPow(x, (SlothPermutation.p - BigInt(1)) / BigInt(2), SlothPermutation.p) === BigInt(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
modSqrtOp(x) {
|
|
36
|
+
let y;
|
|
37
|
+
let value = x;
|
|
38
|
+
|
|
39
|
+
if (this.quadRes(value)) {
|
|
40
|
+
y = this.fastPow(value, (SlothPermutation.p + BigInt(1)) / BigInt(4), SlothPermutation.p);
|
|
41
|
+
} else {
|
|
42
|
+
value = (-value + SlothPermutation.p) % SlothPermutation.p;
|
|
43
|
+
y = this.fastPow(value, (SlothPermutation.p + BigInt(1)) / BigInt(4), SlothPermutation.p);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return y;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
modOp(x, t) {
|
|
50
|
+
let value = x % SlothPermutation.p;
|
|
51
|
+
|
|
52
|
+
for (let i = BigInt(0); i < t; i += BigInt(1)) {
|
|
53
|
+
value = this.modSqrtOp(value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
modVerif(y, x, t) {
|
|
60
|
+
const input = x % SlothPermutation.p;
|
|
61
|
+
let value = y;
|
|
62
|
+
|
|
63
|
+
for (let i = BigInt(0); i < t; i += BigInt(1)) {
|
|
64
|
+
value = (value ** BigInt(2)) % SlothPermutation.p;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!this.quadRes(value)) {
|
|
68
|
+
value = (-value + SlothPermutation.p) % SlothPermutation.p;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return input === value || ((-input + SlothPermutation.p) % SlothPermutation.p) === value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
generateProofVDF(t, x) {
|
|
75
|
+
return this.modOp(x, t);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
verifyProofVDF(t, x, y) {
|
|
79
|
+
return this.modVerif(y, x, t);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = SlothPermutation;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* VDF wrapper adaptation based on:
|
|
3
|
+
* https://github.com/hyperhyperspace/pulsar/blob/main/src/model/VDF.ts
|
|
4
|
+
*/
|
|
5
|
+
const SlothPermutation = require('./sloth-vdf');
|
|
6
|
+
|
|
7
|
+
class VDF {
|
|
8
|
+
static async compute(challengeHex, steps) {
|
|
9
|
+
const vdfInstance = new SlothPermutation();
|
|
10
|
+
const challengeBigInt = BigInt(`0x${challengeHex}`);
|
|
11
|
+
const result = vdfInstance.generateProofVDF(steps, challengeBigInt);
|
|
12
|
+
return result.toString(16);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static async verify(challengeHex, steps, resultHex) {
|
|
16
|
+
const vdfInstance = new SlothPermutation();
|
|
17
|
+
const challengeBigInt = BigInt(`0x${challengeHex}`);
|
|
18
|
+
const resultBigInt = BigInt(`0x${resultHex}`);
|
|
19
|
+
return vdfInstance.verifyProofVDF(steps, challengeBigInt, resultBigInt);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = VDF;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const SignalingPool = require('./signaling-pool');
|
|
2
|
+
const WebSocketSignalingProvider = require('./websocket-signaling-provider');
|
|
3
|
+
const {
|
|
4
|
+
DEFAULT_CLOUDFLARE_SIGNALING_URLS,
|
|
5
|
+
DEFAULT_SIGNALING_FALLBACK_URLS
|
|
6
|
+
} = require('./default-signaling-config');
|
|
7
|
+
|
|
8
|
+
function createDefaultSignalingPool(options = {}) {
|
|
9
|
+
const cloudflareUrls = options.cloudflareUrls || DEFAULT_CLOUDFLARE_SIGNALING_URLS;
|
|
10
|
+
const fallbackUrls = options.fallbackUrls || DEFAULT_SIGNALING_FALLBACK_URLS;
|
|
11
|
+
const WebSocketImpl = options.WebSocketImpl;
|
|
12
|
+
|
|
13
|
+
const providers = [];
|
|
14
|
+
|
|
15
|
+
cloudflareUrls.forEach((url, index) => {
|
|
16
|
+
providers.push(
|
|
17
|
+
new WebSocketSignalingProvider({
|
|
18
|
+
id: `cloudflare-${index + 1}`,
|
|
19
|
+
url,
|
|
20
|
+
WebSocketImpl,
|
|
21
|
+
priority: index
|
|
22
|
+
})
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
fallbackUrls.forEach((url, index) => {
|
|
27
|
+
providers.push(
|
|
28
|
+
new WebSocketSignalingProvider({
|
|
29
|
+
id: `fallback-${index + 1}`,
|
|
30
|
+
url,
|
|
31
|
+
WebSocketImpl,
|
|
32
|
+
priority: cloudflareUrls.length + index
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (Array.isArray(options.customProviders)) {
|
|
38
|
+
providers.push(...options.customProviders);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return new SignalingPool(providers);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = createDefaultSignalingPool;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const DEFAULT_CLOUDFLARE_SIGNALING_URLS = [
|
|
2
|
+
'wss://signaling.cloudflare.com',
|
|
3
|
+
'wss://cloudflare-webrtc-signaling.example',
|
|
4
|
+
'wss://trycloudflare-signaling.example'
|
|
5
|
+
];
|
|
6
|
+
|
|
7
|
+
const DEFAULT_SIGNALING_FALLBACK_URLS = [
|
|
8
|
+
'wss://relay.dignity.dev/signaling',
|
|
9
|
+
'wss://backup-relay.dignity.dev/signaling'
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
DEFAULT_CLOUDFLARE_SIGNALING_URLS,
|
|
14
|
+
DEFAULT_SIGNALING_FALLBACK_URLS
|
|
15
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
class SignalingPool {
|
|
2
|
+
constructor(providers = []) {
|
|
3
|
+
this.providers = [...providers];
|
|
4
|
+
this.activeProvider = null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
registerProvider(provider) {
|
|
8
|
+
this.providers.push(provider);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
getProvidersByPriority() {
|
|
12
|
+
return [...this.providers].sort((a, b) => (a.priority || 0) - (b.priority || 0));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async connect(excludedProviderIds = new Set()) {
|
|
16
|
+
const providers = this.getProvidersByPriority().filter(
|
|
17
|
+
(provider) => !excludedProviderIds.has(provider.id)
|
|
18
|
+
);
|
|
19
|
+
let lastError;
|
|
20
|
+
|
|
21
|
+
for (const provider of providers) {
|
|
22
|
+
try {
|
|
23
|
+
await provider.connect();
|
|
24
|
+
this.activeProvider = provider;
|
|
25
|
+
return provider;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
lastError = error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
throw lastError || new Error('No signaling provider could connect');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async send(message) {
|
|
35
|
+
if (!this.activeProvider) {
|
|
36
|
+
await this.connect();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await this.activeProvider.send(message);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (this.activeProvider && typeof this.activeProvider.disconnect === 'function') {
|
|
43
|
+
await this.activeProvider.disconnect();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const failedProviderId = this.activeProvider ? this.activeProvider.id : null;
|
|
47
|
+
this.activeProvider = null;
|
|
48
|
+
const excludedProviderIds = failedProviderId ? new Set([failedProviderId]) : new Set();
|
|
49
|
+
await this.connect(excludedProviderIds);
|
|
50
|
+
await this.activeProvider.send(message);
|
|
51
|
+
return error;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onMessage(handler) {
|
|
58
|
+
for (const provider of this.providers) {
|
|
59
|
+
if (typeof provider.onMessage === 'function') {
|
|
60
|
+
provider.onMessage(handler);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async disconnect() {
|
|
66
|
+
const disconnections = this.providers
|
|
67
|
+
.filter((provider) => typeof provider.disconnect === 'function')
|
|
68
|
+
.map((provider) => provider.disconnect());
|
|
69
|
+
|
|
70
|
+
await Promise.all(disconnections);
|
|
71
|
+
this.activeProvider = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = SignalingPool;
|