dignity.js 0.1.2 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dignity.js",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "P2P object API for decentralized JavaScript applications",
5
5
  "main": "dist/dignity.cjs.js",
6
6
  "module": "dist/dignity.esm.js",
@@ -23,6 +23,7 @@
23
23
  "scripts": {
24
24
  "test": "jest --coverage",
25
25
  "test:unit": "jest tests/unit --runInBand",
26
+ "test:cloudflare-live": "RUN_CLOUDFLARE_LIVE_TESTS=1 jest tests/integration/cloudflare-signaling-live.test.js --runInBand",
26
27
  "test:pow-calibrate": "jest tests/unit/sloth-vdf-timing.test.js --runInBand",
27
28
  "build": "node scripts/build.js",
28
29
  "docs:serve": "npx http-server docs -p 4173 -o",
@@ -38,12 +39,13 @@
38
39
  "rest",
39
40
  "objects"
40
41
  ],
41
- "license": "MIT",
42
+ "license": "Apache 2.0",
42
43
  "devDependencies": {
43
44
  "esbuild": "^0.28.0",
44
45
  "jest": "^29.7.0"
45
46
  },
46
47
  "dependencies": {
48
+ "peerjs": "^1.5.5",
47
49
  "tweetnacl": "^1.0.3",
48
50
  "tweetnacl-util": "^0.15.1"
49
51
  },
package/src/index.js CHANGED
@@ -10,6 +10,7 @@ const DignityP2P = require('./core/dignity-p2p');
10
10
  const createDefaultSignalingPool = require('./signaling/create-default-signaling-pool');
11
11
  const SignalingPool = require('./signaling/signaling-pool');
12
12
  const WebSocketSignalingProvider = require('./signaling/websocket-signaling-provider');
13
+ const PeerJSSignalingProvider = require('./signaling/peerjs-signaling-provider');
13
14
  const {
14
15
  InMemoryNetworkHub,
15
16
  InMemoryNetworkAdapter
@@ -30,6 +31,7 @@ module.exports = {
30
31
  createDefaultSignalingPool,
31
32
  SignalingPool,
32
33
  WebSocketSignalingProvider,
34
+ PeerJSSignalingProvider,
33
35
  InMemoryNetworkHub,
34
36
  InMemoryNetworkAdapter,
35
37
  DEFAULT_CLOUDFLARE_SIGNALING_URLS,
@@ -12,7 +12,8 @@ const DEFAULT_SECURITY_OPTIONS = {
12
12
  broadcastPasswords: {},
13
13
  resolveBroadcastPassword: null,
14
14
  powSteps: 22,
15
- trustedPeerKeys: {}
15
+ trustedPeerKeys: {},
16
+ kdfIterations: 100000
16
17
  };
17
18
 
18
19
  function stableStringify(value) {
@@ -47,6 +48,37 @@ function utf8ToBytes(value) {
47
48
  return naclUtil.decodeUTF8(value);
48
49
  }
49
50
 
51
+ async function deriveBroadcastKey(password, salt, iterations) {
52
+ const subtle = globalThis.crypto && globalThis.crypto.subtle;
53
+
54
+ if (subtle) {
55
+ const keyMaterial = await subtle.importKey(
56
+ 'raw',
57
+ utf8ToBytes(password),
58
+ 'PBKDF2',
59
+ false,
60
+ ['deriveBits']
61
+ );
62
+ const bits = await subtle.deriveBits(
63
+ { name: 'PBKDF2', salt, iterations, hash: 'SHA-256' },
64
+ keyMaterial,
65
+ 256
66
+ );
67
+ return new Uint8Array(bits);
68
+ }
69
+
70
+ try {
71
+ const { pbkdf2Sync } = require('crypto');
72
+ return new Uint8Array(pbkdf2Sync(password, Buffer.from(salt), iterations, 32, 'sha256'));
73
+ } catch (_ignored) {
74
+ return hash32(concatBytes(utf8ToBytes(password), salt));
75
+ }
76
+ }
77
+
78
+ function legacyBroadcastKey(password, salt) {
79
+ return hash32(concatBytes(utf8ToBytes(password), salt));
80
+ }
81
+
50
82
  function normalizePeerPublicKey(publicKey) {
51
83
  if (!publicKey || typeof publicKey !== 'object') {
52
84
  throw new Error('Public key must be an object with signingPublicKey and encryptionPublicKey');
@@ -153,6 +185,7 @@ class MessageSecurityService {
153
185
  const pow = await this.generatePow(envelope);
154
186
  envelope.security.pow = {
155
187
  enabled: true,
188
+ messageHash: pow.messageHash,
156
189
  challenge: pow.challenge,
157
190
  proof: pow.proof,
158
191
  steps: pow.steps,
@@ -208,6 +241,10 @@ class MessageSecurityService {
208
241
  });
209
242
  }
210
243
 
244
+ computePowMessageHash(envelope) {
245
+ return bytesToHex(hash32(utf8ToBytes(this.canonicalPowInput(envelope))));
246
+ }
247
+
211
248
  async decryptIncomingMessage(envelope) {
212
249
  if (!this.options.enabled) {
213
250
  return {
@@ -235,7 +272,7 @@ class MessageSecurityService {
235
272
  this.verifySignature(envelope);
236
273
  }
237
274
 
238
- const payload = this.decryptPayload(envelope);
275
+ const payload = await this.decryptPayload(envelope);
239
276
 
240
277
  return {
241
278
  ignored: false,
@@ -315,7 +352,8 @@ class MessageSecurityService {
315
352
  const nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
316
353
  const salt = nacl.randomBytes(16);
317
354
  const password = this.resolveBroadcastPassword(scope);
318
- const key = hash32(concatBytes(utf8ToBytes(password), salt));
355
+ const iterations = this.options.kdfIterations || DEFAULT_SECURITY_OPTIONS.kdfIterations;
356
+ const key = await deriveBroadcastKey(password, salt, iterations);
319
357
  const encrypted = nacl.secretbox(plainText, nonce, key);
320
358
 
321
359
  return {
@@ -325,12 +363,14 @@ class MessageSecurityService {
325
363
  mode: 'broadcast',
326
364
  scope,
327
365
  nonce: naclUtil.encodeBase64(nonce),
328
- salt: naclUtil.encodeBase64(salt)
366
+ salt: naclUtil.encodeBase64(salt),
367
+ kdf: 'pbkdf2',
368
+ kdfIterations: iterations
329
369
  }
330
370
  };
331
371
  }
332
372
 
333
- decryptPayload(envelope) {
373
+ async decryptPayload(envelope) {
334
374
  const encryption = envelope.security ? envelope.security.encryption : null;
335
375
 
336
376
  if (!encryption || !encryption.enabled || !this.options.encryptionEnabled) {
@@ -344,7 +384,15 @@ class MessageSecurityService {
344
384
  const password = this.resolveBroadcastPassword(scope);
345
385
  const salt = naclUtil.decodeBase64(encryption.salt);
346
386
  const nonce = naclUtil.decodeBase64(encryption.nonce);
347
- const key = hash32(concatBytes(utf8ToBytes(password), salt));
387
+
388
+ let key;
389
+ if (encryption.kdf === 'pbkdf2') {
390
+ const iterations = encryption.kdfIterations || DEFAULT_SECURITY_OPTIONS.kdfIterations;
391
+ key = await deriveBroadcastKey(password, salt, iterations);
392
+ } else {
393
+ key = legacyBroadcastKey(password, salt);
394
+ }
395
+
348
396
  const decrypted = nacl.secretbox.open(encryptedBuffer, nonce, key);
349
397
 
350
398
  if (!decrypted) {
@@ -420,13 +468,15 @@ class MessageSecurityService {
420
468
  }
421
469
 
422
470
  async generatePow(envelope) {
423
- const challenge = bytesToHex(hash32(utf8ToBytes(this.canonicalPowInput(envelope))));
471
+ const messageHash = this.computePowMessageHash(envelope);
472
+ const challenge = messageHash;
424
473
  const steps = await this.determinePowSteps();
425
474
  const start = this.now();
426
475
  const proof = await VDF.compute(challenge, steps);
427
476
  const durationMs = this.now() - start;
428
477
 
429
478
  return {
479
+ messageHash,
430
480
  challenge,
431
481
  proof,
432
482
  steps: steps.toString(),
@@ -435,16 +485,21 @@ class MessageSecurityService {
435
485
  }
436
486
 
437
487
  async verifyPow(envelope) {
438
- const expectedChallenge = bytesToHex(hash32(utf8ToBytes(this.canonicalPowInput(envelope))));
488
+ const expectedMessageHash = this.computePowMessageHash(envelope);
439
489
  const pow = envelope.security.pow;
440
490
 
441
- if (!pow || pow.challenge !== expectedChallenge) {
491
+ if (
492
+ !pow ||
493
+ !pow.messageHash ||
494
+ pow.messageHash !== expectedMessageHash ||
495
+ pow.challenge !== pow.messageHash
496
+ ) {
442
497
  const error = new Error('PoW challenge mismatch');
443
498
  error.code = 'INVALID_POW';
444
499
  throw error;
445
500
  }
446
501
 
447
- const verified = await VDF.verify(pow.challenge, BigInt(pow.steps), pow.proof);
502
+ const verified = await VDF.verify(pow.messageHash, BigInt(pow.steps), pow.proof);
448
503
  if (!verified) {
449
504
  const error = new Error('PoW verification failed');
450
505
  error.code = 'INVALID_POW';
@@ -458,5 +513,7 @@ class MessageSecurityService {
458
513
  module.exports = {
459
514
  MessageSecurityService,
460
515
  stableStringify,
516
+ deriveBroadcastKey,
517
+ legacyBroadcastKey,
461
518
  DEFAULT_SECURITY_OPTIONS
462
519
  };
@@ -1,5 +1,6 @@
1
1
  const SignalingPool = require('./signaling-pool');
2
2
  const WebSocketSignalingProvider = require('./websocket-signaling-provider');
3
+ const PeerJSSignalingProvider = require('./peerjs-signaling-provider');
3
4
  const {
4
5
  DEFAULT_CLOUDFLARE_SIGNALING_URLS,
5
6
  DEFAULT_SIGNALING_FALLBACK_URLS
@@ -13,8 +14,11 @@ function createDefaultSignalingPool(options = {}) {
13
14
  const providers = [];
14
15
 
15
16
  cloudflareUrls.forEach((url, index) => {
17
+ const usePeerJsProvider = /^wss:\/\/(peerjs\.92k\.de|0\.peerjs\.com)(\/|$)/.test(url);
18
+ const ProviderClass = usePeerJsProvider ? PeerJSSignalingProvider : WebSocketSignalingProvider;
19
+
16
20
  providers.push(
17
- new WebSocketSignalingProvider({
21
+ new ProviderClass({
18
22
  id: `cloudflare-${index + 1}`,
19
23
  url,
20
24
  WebSocketImpl,
@@ -1,7 +1,6 @@
1
1
  const DEFAULT_CLOUDFLARE_SIGNALING_URLS = [
2
- 'wss://signaling.cloudflare.com',
3
- 'wss://cloudflare-webrtc-signaling.example',
4
- 'wss://trycloudflare-signaling.example'
2
+ 'wss://peerjs.92k.de/peerjs?key=peerjs',
3
+ 'wss://0.peerjs.com/peerjs?key=peerjs'
5
4
  ];
6
5
 
7
6
  const DEFAULT_SIGNALING_FALLBACK_URLS = [
@@ -0,0 +1,210 @@
1
+ const WebSocketSignalingProvider = require('./websocket-signaling-provider');
2
+
3
+ class PeerJSSignalingProvider {
4
+ constructor({ id, url, PeerImpl, WebSocketImpl, priority = 0, connectTimeoutMs = 10000 }) {
5
+ if (!url) {
6
+ throw new Error('PeerJS signaling provider requires a url');
7
+ }
8
+
9
+ this.id = id || url;
10
+ this.url = url;
11
+ this.priority = priority;
12
+ this.isCustomPeerImpl = Boolean(PeerImpl);
13
+ this.PeerImpl = PeerImpl || this.resolvePeerImplementation();
14
+ this.WebSocketImpl = WebSocketImpl;
15
+ this.connectTimeoutMs = connectTimeoutMs;
16
+ this.peer = null;
17
+ this.peerId = null;
18
+ this.connections = new Map();
19
+ this.messageHandlers = new Set();
20
+ this.fallbackProvider = null;
21
+ }
22
+
23
+ resolvePeerImplementation() {
24
+ try {
25
+ const peerjs = require('peerjs');
26
+ return peerjs.Peer || peerjs;
27
+ } catch (error) {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ parsePeerJsServerUrl() {
33
+ const parsed = new URL(this.url);
34
+ const secure = parsed.protocol === 'wss:';
35
+ const host = parsed.hostname;
36
+ const port = parsed.port ? Number(parsed.port) : secure ? 443 : 80;
37
+ const path = parsed.pathname || '/';
38
+ const key = parsed.searchParams.get('key') || 'peerjs';
39
+
40
+ return { secure, host, port, path, key };
41
+ }
42
+
43
+ shouldUseWebSocketFallback() {
44
+ return !this.isCustomPeerImpl && typeof globalThis.RTCPeerConnection !== 'function';
45
+ }
46
+
47
+ useWebSocketFallback() {
48
+ if (!this.fallbackProvider) {
49
+ this.fallbackProvider = new WebSocketSignalingProvider({
50
+ id: `${this.id}-ws-fallback`,
51
+ url: this.url,
52
+ WebSocketImpl: this.WebSocketImpl,
53
+ priority: this.priority
54
+ });
55
+ }
56
+ return this.fallbackProvider;
57
+ }
58
+
59
+ async connect() {
60
+ if (this.shouldUseWebSocketFallback()) {
61
+ await this.useWebSocketFallback().connect();
62
+ return;
63
+ }
64
+
65
+ if (!this.PeerImpl) {
66
+ throw new Error('PeerJS implementation is not available');
67
+ }
68
+
69
+ const server = this.parsePeerJsServerUrl();
70
+ const peerId = `dignityjs_${Math.random().toString(36).slice(2, 12)}`;
71
+
72
+ await new Promise((resolve, reject) => {
73
+ const peer = new this.PeerImpl(peerId, {
74
+ host: server.host,
75
+ port: server.port,
76
+ path: server.path,
77
+ secure: server.secure,
78
+ key: server.key
79
+ });
80
+
81
+ const timeout = setTimeout(() => {
82
+ reject(new Error(`Unable to connect to signaling url ${this.url}`));
83
+ }, this.connectTimeoutMs);
84
+
85
+ peer.on('open', () => {
86
+ clearTimeout(timeout);
87
+ this.peer = peer;
88
+ this.peerId = peerId;
89
+ resolve();
90
+ });
91
+
92
+ peer.on('connection', (connection) => {
93
+ this.attachConnectionHandlers(connection);
94
+ });
95
+
96
+ peer.on('error', async (error) => {
97
+ clearTimeout(timeout);
98
+ if (error && error.type === 'browser-incompatible') {
99
+ try {
100
+ await this.useWebSocketFallback().connect();
101
+ resolve();
102
+ return;
103
+ } catch (fallbackError) {
104
+ reject(new Error(`Unable to connect to signaling url ${this.url}`));
105
+ return;
106
+ }
107
+ }
108
+ reject(new Error(`Unable to connect to signaling url ${this.url}`));
109
+ });
110
+ });
111
+ }
112
+
113
+ attachConnectionHandlers(connection) {
114
+ const remoteId = connection.peer;
115
+ this.connections.set(remoteId, connection);
116
+
117
+ connection.on('data', (payload) => {
118
+ for (const handler of this.messageHandlers) {
119
+ handler(payload);
120
+ }
121
+ });
122
+
123
+ connection.on('close', () => {
124
+ this.connections.delete(remoteId);
125
+ });
126
+ }
127
+
128
+ async openConnection(remotePeerId) {
129
+ if (!this.peer) {
130
+ throw new Error('PeerJS is not connected');
131
+ }
132
+
133
+ const existing = this.connections.get(remotePeerId);
134
+ if (existing && existing.open) {
135
+ return existing;
136
+ }
137
+
138
+ return await new Promise((resolve, reject) => {
139
+ const connection = this.peer.connect(remotePeerId, { reliable: true, serialization: 'json' });
140
+ const timeout = setTimeout(() => {
141
+ reject(new Error(`Unable to connect peer ${remotePeerId} via ${this.url}`));
142
+ }, this.connectTimeoutMs);
143
+
144
+ connection.on('open', () => {
145
+ clearTimeout(timeout);
146
+ this.attachConnectionHandlers(connection);
147
+ resolve(connection);
148
+ });
149
+
150
+ connection.on('error', () => {
151
+ clearTimeout(timeout);
152
+ reject(new Error(`Unable to connect peer ${remotePeerId} via ${this.url}`));
153
+ });
154
+ });
155
+ }
156
+
157
+ onMessage(handler) {
158
+ if (this.fallbackProvider) {
159
+ this.fallbackProvider.onMessage(handler);
160
+ return;
161
+ }
162
+ this.messageHandlers.add(handler);
163
+ }
164
+
165
+ async send(message) {
166
+ if (this.fallbackProvider) {
167
+ await this.fallbackProvider.send(message);
168
+ return;
169
+ }
170
+
171
+ if (!this.peer) {
172
+ throw new Error(`Signaling socket is not open for ${this.url}`);
173
+ }
174
+
175
+ if (message && message.to) {
176
+ const connection = await this.openConnection(message.to);
177
+ connection.send(message);
178
+ return;
179
+ }
180
+
181
+ for (const connection of this.connections.values()) {
182
+ if (connection.open) {
183
+ connection.send(message);
184
+ }
185
+ }
186
+ }
187
+
188
+ async disconnect() {
189
+ if (this.fallbackProvider) {
190
+ await this.fallbackProvider.disconnect();
191
+ this.fallbackProvider = null;
192
+ return;
193
+ }
194
+
195
+ for (const connection of this.connections.values()) {
196
+ if (typeof connection.close === 'function') {
197
+ connection.close();
198
+ }
199
+ }
200
+ this.connections.clear();
201
+
202
+ if (this.peer && typeof this.peer.destroy === 'function') {
203
+ this.peer.destroy();
204
+ }
205
+ this.peer = null;
206
+ this.peerId = null;
207
+ }
208
+ }
209
+
210
+ module.exports = PeerJSSignalingProvider;
@@ -18,7 +18,7 @@ class WebSocketSignalingProvider {
18
18
  }
19
19
 
20
20
  await new Promise((resolve, reject) => {
21
- const socket = new this.WebSocketImpl(this.url);
21
+ const socket = new this.WebSocketImpl(this.buildConnectionUrl());
22
22
 
23
23
  socket.onopen = () => {
24
24
  this.socket = socket;
@@ -44,6 +44,29 @@ class WebSocketSignalingProvider {
44
44
  });
45
45
  }
46
46
 
47
+ buildConnectionUrl() {
48
+ const peerJsHostPattern = /^wss:\/\/(peerjs\.92k\.de|0\.peerjs\.com)(\/|$)/;
49
+ if (!peerJsHostPattern.test(this.url)) {
50
+ return this.url;
51
+ }
52
+
53
+ const connectionId = `dignityjs_${Math.random().toString(36).slice(2, 12)}`;
54
+ const token = Math.random().toString(36).slice(2, 12);
55
+ const hasQuery = this.url.includes('?');
56
+ const hasId = /[?&]id=/.test(this.url);
57
+ const hasToken = /[?&]token=/.test(this.url);
58
+
59
+ let url = this.url;
60
+ if (!hasId) {
61
+ url += `${hasQuery ? '&' : '?'}id=${connectionId}`;
62
+ }
63
+ if (!hasToken) {
64
+ url += `${url.includes('?') ? '&' : '?'}token=${token}`;
65
+ }
66
+
67
+ return url;
68
+ }
69
+
47
70
  onMessage(handler) {
48
71
  this.messageHandlers.add(handler);
49
72
  }