uniwrtc 2.0.0 → 2.0.2
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 +3 -2
- package/src/crypto.js +96 -0
- package/src/main.js +184 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniwrtc",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "A universal WebRTC signaling service",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"author": "",
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"nostr-tools": "^2.9.0"
|
|
30
|
+
"nostr-tools": "^2.9.0",
|
|
31
|
+
"unsea": "^1.1.2"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@playwright/test": "^1.57.0",
|
package/src/crypto.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { encryptMessageWithMeta, decryptMessageWithMeta } from 'unsea';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Encryption utilities for signaling messages using unsea (ECIES)
|
|
5
|
+
* Uses elliptic curve cryptography for secure peer-to-peer encryption
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Store peer public keys by peer ID (hex)
|
|
9
|
+
const peerPublicKeys = new Map();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Register a peer's public key (hex string)
|
|
13
|
+
* @param {string} peerId - Peer ID (hex)
|
|
14
|
+
* @param {object} publicKeyObj - Public key object from unsea
|
|
15
|
+
*/
|
|
16
|
+
export function registerPeerPublicKey(peerId, publicKeyObj) {
|
|
17
|
+
peerPublicKeys.set(peerId, publicKeyObj);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get a peer's public key
|
|
22
|
+
* @param {string} peerId - Peer ID (hex)
|
|
23
|
+
* @returns {object|null} Public key object or null
|
|
24
|
+
*/
|
|
25
|
+
export function getPeerPublicKey(peerId) {
|
|
26
|
+
return peerPublicKeys.get(peerId) || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Encrypt a JSON payload for transmission to a specific peer
|
|
31
|
+
* @param {object} payload - Object to encrypt
|
|
32
|
+
* @param {object} recipientPublicKey - Recipient's public key object from unsea
|
|
33
|
+
* @returns {Promise<object>} Encrypted message with metadata
|
|
34
|
+
*/
|
|
35
|
+
export async function encryptPayload(payload, recipientPublicKey) {
|
|
36
|
+
try {
|
|
37
|
+
const plaintext = JSON.stringify(payload);
|
|
38
|
+
const encrypted = await encryptMessageWithMeta(plaintext, recipientPublicKey);
|
|
39
|
+
return encrypted;
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error('[Crypto] Encryption failed:', e);
|
|
42
|
+
throw e;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Decrypt a payload using unsea
|
|
48
|
+
* @param {object} encrypted - Encrypted message object
|
|
49
|
+
* @param {object} senderPublicKey - Sender's public key object
|
|
50
|
+
* @param {object} myPrivateKey - This peer's private key object
|
|
51
|
+
* @returns {Promise<object>} Decrypted JSON payload
|
|
52
|
+
*/
|
|
53
|
+
export async function decryptPayload(encrypted, senderPublicKey, myPrivateKey) {
|
|
54
|
+
try {
|
|
55
|
+
const plaintext = await decryptMessageWithMeta(encrypted, senderPublicKey, myPrivateKey);
|
|
56
|
+
return JSON.parse(plaintext);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.error('[Crypto] Decryption failed:', e);
|
|
59
|
+
throw e;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Wrap a payload for encrypted transmission
|
|
65
|
+
* @param {object} payload - Payload to wrap
|
|
66
|
+
* @param {object} recipientPublicKey - Recipient's public key object from unsea
|
|
67
|
+
* @returns {Promise<object>} Wrapped envelope with encrypted content
|
|
68
|
+
*/
|
|
69
|
+
export async function wrapEncryptedPayload(payload, recipientPublicKey) {
|
|
70
|
+
const encrypted = await encryptPayload(payload, recipientPublicKey);
|
|
71
|
+
return {
|
|
72
|
+
type: 'encrypted',
|
|
73
|
+
encrypted: true,
|
|
74
|
+
content: JSON.stringify(encrypted),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Unwrap an encrypted payload
|
|
80
|
+
* @param {object} envelope - Encrypted envelope
|
|
81
|
+
* @param {object} senderPublicKey - Sender's public key object
|
|
82
|
+
* @param {object} myPrivateKey - This peer's private key object
|
|
83
|
+
* @returns {Promise<object>} Decrypted payload
|
|
84
|
+
*/
|
|
85
|
+
export async function unwrapEncryptedPayload(envelope, senderPublicKey, myPrivateKey) {
|
|
86
|
+
if (!envelope.encrypted) {
|
|
87
|
+
throw new Error('Payload is not encrypted');
|
|
88
|
+
}
|
|
89
|
+
const encrypted = JSON.parse(envelope.content);
|
|
90
|
+
return decryptPayload(encrypted, senderPublicKey, myPrivateKey);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Dummy function for compatibility
|
|
94
|
+
export function deriveSharedSecret(pubkey1, pubkey2) {
|
|
95
|
+
return { pubkey1, pubkey2 };
|
|
96
|
+
}
|
package/src/main.js
CHANGED
|
@@ -2,6 +2,8 @@ import './style.css';
|
|
|
2
2
|
import UniWRTCClient from '../client-browser.js';
|
|
3
3
|
import { createNostrClient } from './nostr/nostrClient.js';
|
|
4
4
|
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure';
|
|
5
|
+
import { wrapEncryptedPayload, unwrapEncryptedPayload, deriveSharedSecret, registerPeerPublicKey, getPeerPublicKey } from './crypto.js';
|
|
6
|
+
import { generateRandomPair } from 'unsea';
|
|
5
7
|
|
|
6
8
|
// Make UniWRTCClient available globally for backwards compatibility
|
|
7
9
|
window.UniWRTCClient = UniWRTCClient;
|
|
@@ -10,6 +12,8 @@ window.UniWRTCClient = UniWRTCClient;
|
|
|
10
12
|
let nostrClient = null;
|
|
11
13
|
let myPeerId = null;
|
|
12
14
|
let mySessionNonce = null;
|
|
15
|
+
let encryptionEnabled = true; // Encryption toggle - defaults to ON
|
|
16
|
+
let myKeyPair = null; // unsea key pair for encryption { publicKey, privateKey }
|
|
13
17
|
const peerSessions = new Map();
|
|
14
18
|
const peerProbeState = new Map();
|
|
15
19
|
const peerResyncState = new Map();
|
|
@@ -76,10 +80,16 @@ document.getElementById('app').innerHTML = `
|
|
|
76
80
|
<input type="text" id="roomId" data-testid="roomId" placeholder="my-room">
|
|
77
81
|
</div>
|
|
78
82
|
</div>
|
|
79
|
-
<div style="display: flex; gap: 10px; align-items: center;">
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
+
<div style="display: flex; gap: 10px; align-items: center; justify-content: space-between;">
|
|
84
|
+
<div style="display: flex; gap: 10px; align-items: center;">
|
|
85
|
+
<button onclick="window.connect()" class="btn-primary" id="connectBtn" data-testid="connectBtn">Connect</button>
|
|
86
|
+
<button onclick="window.disconnect()" class="btn-danger" id="disconnectBtn" data-testid="disconnectBtn" disabled>Disconnect</button>
|
|
87
|
+
<span id="statusBadge" data-testid="statusBadge" class="status-badge status-disconnected">Disconnected</span>
|
|
88
|
+
</div>
|
|
89
|
+
<label style="display: flex; align-items: center; gap: 8px; color: #64748b; font-size: 13px; white-space: nowrap;">
|
|
90
|
+
<input type="checkbox" id="encryptionToggle" onchange="window.toggleEncryption()" checked style="cursor: pointer; width: 16px; height: 16px;">
|
|
91
|
+
Encrypt
|
|
92
|
+
</label>
|
|
83
93
|
</div>
|
|
84
94
|
</div>
|
|
85
95
|
|
|
@@ -97,11 +107,6 @@ document.getElementById('app').innerHTML = `
|
|
|
97
107
|
</div>
|
|
98
108
|
</div>
|
|
99
109
|
|
|
100
|
-
<div class="card">
|
|
101
|
-
<h2>Role</h2>
|
|
102
|
-
<div id="roleBadge" data-testid="roleBadge" class="status-badge status-disconnected">Not assigned</div>
|
|
103
|
-
</div>
|
|
104
|
-
|
|
105
110
|
<div class="card">
|
|
106
111
|
<h2>Connected Peers</h2>
|
|
107
112
|
<div id="peerList" data-testid="peerList" class="peer-list">
|
|
@@ -129,6 +134,16 @@ document.getElementById('app').innerHTML = `
|
|
|
129
134
|
</div>
|
|
130
135
|
`;
|
|
131
136
|
|
|
137
|
+
// FORCE ENCRYPTION DEFAULT ON IMMEDIATELY AFTER HTML IS CREATED
|
|
138
|
+
const encryptCb = document.getElementById('encryptionToggle');
|
|
139
|
+
if (encryptCb) {
|
|
140
|
+
console.log('[ENCRYPTION] Found checkbox, forcing ON');
|
|
141
|
+
encryptCb.defaultChecked = true;
|
|
142
|
+
encryptCb.checked = true;
|
|
143
|
+
encryptionEnabled = true;
|
|
144
|
+
console.log('[ENCRYPTION] Set encryptionEnabled=true, checkbox.checked=' + encryptCb.checked);
|
|
145
|
+
}
|
|
146
|
+
|
|
132
147
|
// Prefill room input from URL (?room= or ?session=); otherwise set a visible default the user can override
|
|
133
148
|
const roomInput = document.getElementById('roomId');
|
|
134
149
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -155,6 +170,16 @@ roomInput.addEventListener('input', () => {
|
|
|
155
170
|
document.getElementById('sessionId').textContent = roomInput.value.trim() || 'Not joined';
|
|
156
171
|
});
|
|
157
172
|
|
|
173
|
+
// Force encryption toggle to default ON on load
|
|
174
|
+
const encryptionCheckbox = document.getElementById('encryptionToggle');
|
|
175
|
+
if (encryptionCheckbox) {
|
|
176
|
+
encryptionCheckbox.defaultChecked = true; // force default state
|
|
177
|
+
encryptionCheckbox.checked = true; // override any persisted form state
|
|
178
|
+
encryptionEnabled = true;
|
|
179
|
+
window.toggleEncryption?.(); // ensure UI + state sync on load
|
|
180
|
+
log('Signaling encryption defaulted to ON', 'success');
|
|
181
|
+
}
|
|
182
|
+
|
|
158
183
|
// ICE servers: STUN-only by default (no TURN). For deterministic local testing,
|
|
159
184
|
// support host-only ICE via URL flag: ?ice=host (or ?ice=none)
|
|
160
185
|
const iceMode = (params.get('ice') || '').toLowerCase();
|
|
@@ -230,27 +255,6 @@ function updateStatus(connected) {
|
|
|
230
255
|
}
|
|
231
256
|
}
|
|
232
257
|
|
|
233
|
-
function updateRole(role) {
|
|
234
|
-
const badge = document.getElementById('roleBadge');
|
|
235
|
-
if (!badge) return;
|
|
236
|
-
|
|
237
|
-
if (role === 'coordinator') {
|
|
238
|
-
badge.textContent = 'Coordinator';
|
|
239
|
-
badge.className = 'status-badge status-connected';
|
|
240
|
-
// If assigned a role, we must be connected
|
|
241
|
-
updateStatus(true);
|
|
242
|
-
} else if (role === 'peer') {
|
|
243
|
-
badge.textContent = 'Peer';
|
|
244
|
-
badge.className = 'status-badge status-info';
|
|
245
|
-
// If assigned a role, we must be connected
|
|
246
|
-
updateStatus(true);
|
|
247
|
-
} else {
|
|
248
|
-
badge.textContent = 'Not assigned';
|
|
249
|
-
badge.className = 'status-badge status-disconnected';
|
|
250
|
-
updateStatus(false);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
258
|
function updatePeerList() {
|
|
255
259
|
const peerList = document.getElementById('peerList');
|
|
256
260
|
if (!peerList) return;
|
|
@@ -302,32 +306,63 @@ function isPoliteFor(peerId) {
|
|
|
302
306
|
return !shouldInitiateWith(peerId);
|
|
303
307
|
}
|
|
304
308
|
|
|
305
|
-
function sendSignal(to, payload) {
|
|
309
|
+
async function sendSignal(to, payload) {
|
|
306
310
|
if (!nostrClient) throw new Error('Not connected to Nostr');
|
|
307
|
-
|
|
308
|
-
const toSession = peerSessions.get(to);
|
|
309
311
|
const type = payload?.type;
|
|
310
|
-
const
|
|
312
|
+
const isBroadcast = !to;
|
|
313
|
+
const toSession = isBroadcast ? null : peerSessions.get(to);
|
|
314
|
+
const needsToSession = !isBroadcast && type !== 'probe';
|
|
311
315
|
|
|
312
316
|
if (needsToSession && !toSession) throw new Error('No peer session yet');
|
|
313
317
|
|
|
314
|
-
|
|
318
|
+
let finalPayload = {
|
|
315
319
|
...payload,
|
|
316
|
-
to,
|
|
320
|
+
...(to ? { to } : {}),
|
|
317
321
|
...(needsToSession ? { toSession } : {}),
|
|
318
322
|
fromSession: mySessionNonce,
|
|
319
|
-
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// Optionally encrypt the payload using unsea
|
|
326
|
+
if (encryptionEnabled && to && myKeyPair) {
|
|
327
|
+
try {
|
|
328
|
+
const recipientPublicKey = getPeerPublicKey(to);
|
|
329
|
+
if (recipientPublicKey) {
|
|
330
|
+
finalPayload = await wrapEncryptedPayload(finalPayload, recipientPublicKey);
|
|
331
|
+
}
|
|
332
|
+
// Silently fall back to unencrypted if no peer key yet - will be available from their hello
|
|
333
|
+
} catch (e) {
|
|
334
|
+
console.warn('[Crypto] Encryption failed, sending unencrypted:', e?.message);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return nostrClient.send(finalPayload);
|
|
320
339
|
}
|
|
321
340
|
|
|
322
|
-
function sendSignalToSession(to, payload, toSession) {
|
|
341
|
+
async function sendSignalToSession(to, payload, toSession) {
|
|
323
342
|
if (!nostrClient) throw new Error('Not connected to Nostr');
|
|
324
343
|
if (!toSession) throw new Error('toSession is required');
|
|
325
|
-
|
|
344
|
+
|
|
345
|
+
let finalPayload = {
|
|
326
346
|
...payload,
|
|
327
347
|
to,
|
|
328
348
|
toSession,
|
|
329
349
|
fromSession: mySessionNonce,
|
|
330
|
-
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// Optionally encrypt the payload using unsea
|
|
353
|
+
if (encryptionEnabled && to && myKeyPair) {
|
|
354
|
+
try {
|
|
355
|
+
const recipientPublicKey = getPeerPublicKey(to);
|
|
356
|
+
if (recipientPublicKey) {
|
|
357
|
+
finalPayload = await wrapEncryptedPayload(finalPayload, recipientPublicKey);
|
|
358
|
+
}
|
|
359
|
+
// Silently fall back to unencrypted if no peer key yet - will be available from their hello
|
|
360
|
+
} catch (e) {
|
|
361
|
+
console.warn('[Crypto] Encryption failed, sending unencrypted:', e?.message);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return nostrClient.send(finalPayload);
|
|
331
366
|
}
|
|
332
367
|
|
|
333
368
|
async function maybeProbePeer(peerId) {
|
|
@@ -342,7 +377,16 @@ async function maybeProbePeer(peerId) {
|
|
|
342
377
|
const probeId = Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
|
|
343
378
|
peerProbeState.set(peerId, { session, ts: Date.now(), probeId });
|
|
344
379
|
try {
|
|
345
|
-
|
|
380
|
+
const probePayload = { type: 'probe', probeId };
|
|
381
|
+
// Include our encryption public key so peer can encrypt responses
|
|
382
|
+
if (encryptionEnabled && myKeyPair && myKeyPair.publicKey) {
|
|
383
|
+
try {
|
|
384
|
+
probePayload.encryptionPublicKey = JSON.stringify(myKeyPair.publicKey);
|
|
385
|
+
} catch (e) {
|
|
386
|
+
console.warn('[Crypto] Failed to serialize public key for probe:', e?.message);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
await sendSignal(peerId, probePayload);
|
|
346
390
|
log(`Probing peer ${peerId.substring(0, 6)}...`, 'info');
|
|
347
391
|
} catch (e) {
|
|
348
392
|
log(`Probe failed: ${e?.message || e}`, 'warning');
|
|
@@ -486,9 +530,11 @@ async function connectNostr() {
|
|
|
486
530
|
nostrClient = null;
|
|
487
531
|
}
|
|
488
532
|
|
|
489
|
-
//
|
|
533
|
+
// Preserve session across relay reconnects to avoid breaking existing signaling.
|
|
534
|
+
// Only reset on true disconnect (Disconnect button), not on relay failures.
|
|
490
535
|
myPeerId = myPeerId || ensureIdentity();
|
|
491
|
-
mySessionNonce
|
|
536
|
+
// NOTE: Do NOT reset mySessionNonce here. Keep it stable across relay changes.
|
|
537
|
+
// If no session exists yet, it will be created below.
|
|
492
538
|
peerSessions.clear();
|
|
493
539
|
peerProbeState.clear();
|
|
494
540
|
readyPeers.clear();
|
|
@@ -524,7 +570,21 @@ async function connectNostr() {
|
|
|
524
570
|
// Any client instance will derive the same per-tab keypair.
|
|
525
571
|
const myPubkey = myPeerId || ensureIdentity();
|
|
526
572
|
myPeerId = myPubkey;
|
|
527
|
-
|
|
573
|
+
|
|
574
|
+
// Generate unsea key pair for encryption (once per connection, defaults to enabled)
|
|
575
|
+
if (!myKeyPair) {
|
|
576
|
+
try {
|
|
577
|
+
myKeyPair = await generateRandomPair();
|
|
578
|
+
console.log('[Crypto] Generated unsea key pair for peer', myPeerId.substring(0, 6));
|
|
579
|
+
} catch (e) {
|
|
580
|
+
console.warn('[Crypto] Failed to generate key pair:', e?.message);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Generate session nonce only once per connect session (preserve across relay changes)
|
|
585
|
+
if (!mySessionNonce) {
|
|
586
|
+
mySessionNonce = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
587
|
+
}
|
|
528
588
|
document.getElementById('clientId').textContent = myPubkey.substring(0, 16) + '...';
|
|
529
589
|
document.getElementById('sessionId').textContent = effectiveRoom;
|
|
530
590
|
|
|
@@ -533,8 +593,12 @@ async function connectNostr() {
|
|
|
533
593
|
room: effectiveRoom,
|
|
534
594
|
secretKeyHex: mySecretKeyHex,
|
|
535
595
|
onState: (state) => {
|
|
536
|
-
if (state === 'connected')
|
|
537
|
-
|
|
596
|
+
if (state === 'connected') {
|
|
597
|
+
updateStatus(true);
|
|
598
|
+
}
|
|
599
|
+
if (state === 'disconnected') {
|
|
600
|
+
updateStatus(false);
|
|
601
|
+
}
|
|
538
602
|
},
|
|
539
603
|
onNotice: (notice) => {
|
|
540
604
|
log(`Relay NOTICE (${relayUrl}): ${String(notice)}`, 'warning');
|
|
@@ -546,6 +610,27 @@ async function connectNostr() {
|
|
|
546
610
|
const peerId = from;
|
|
547
611
|
if (!peerId || peerId === myPeerId) return;
|
|
548
612
|
|
|
613
|
+
// Try to decrypt if encrypted (using unsea)
|
|
614
|
+
let decrypted = payload;
|
|
615
|
+
if (payload && payload.encrypted && payload.content && myKeyPair && myKeyPair.privateKey) {
|
|
616
|
+
try {
|
|
617
|
+
const senderPublicKey = getPeerPublicKey(peerId);
|
|
618
|
+
if (senderPublicKey) {
|
|
619
|
+
decrypted = await unwrapEncryptedPayload(payload, senderPublicKey, myKeyPair.privateKey);
|
|
620
|
+
console.log('[Crypto] Decrypted message from', peerId.substring(0, 6));
|
|
621
|
+
} else {
|
|
622
|
+
console.warn('[Crypto] No public key for sender, cannot decrypt');
|
|
623
|
+
}
|
|
624
|
+
} catch (e) {
|
|
625
|
+
console.warn('[Crypto] Failed to decrypt message from', peerId.substring(0, 6), ':', e?.message);
|
|
626
|
+
// If decryption fails, ignore the message (safety-first for encrypted content)
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Use decrypted payload for all subsequent processing
|
|
632
|
+
payload = decrypted;
|
|
633
|
+
|
|
549
634
|
// Use the existing peer list UI as a simple "seen peers" list
|
|
550
635
|
if (!peerConnections.has(peerId)) {
|
|
551
636
|
peerConnections.set(peerId, null);
|
|
@@ -574,6 +659,17 @@ async function connectNostr() {
|
|
|
574
659
|
readyPeers.delete(peerId);
|
|
575
660
|
}
|
|
576
661
|
|
|
662
|
+
// Extract peer's encryption public key if included
|
|
663
|
+
if (encryptionEnabled && payload.encryptionPublicKey) {
|
|
664
|
+
try {
|
|
665
|
+
const publicKeyObj = JSON.parse(payload.encryptionPublicKey);
|
|
666
|
+
registerPeerPublicKey(peerId, publicKeyObj);
|
|
667
|
+
console.log('[Crypto] Registered peer encryption key from hello:', peerId.substring(0, 6));
|
|
668
|
+
} catch (e) {
|
|
669
|
+
console.warn('[Crypto] Failed to parse peer public key from hello:', e?.message);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
577
673
|
// We may receive peer presence while still selecting a relay.
|
|
578
674
|
// Store and probe once we have a selected/connected `nostrClient`.
|
|
579
675
|
deferredHelloPeers.add(peerId);
|
|
@@ -631,6 +727,16 @@ async function connectNostr() {
|
|
|
631
727
|
logDrop(peerId, payload, 'probeId mismatch');
|
|
632
728
|
return;
|
|
633
729
|
}
|
|
730
|
+
// Extract peer's encryption public key from probe-ack if included
|
|
731
|
+
if (encryptionEnabled && payload.encryptionPublicKey) {
|
|
732
|
+
try {
|
|
733
|
+
const publicKeyObj = JSON.parse(payload.encryptionPublicKey);
|
|
734
|
+
registerPeerPublicKey(peerId, publicKeyObj);
|
|
735
|
+
console.log('[Crypto] Registered peer encryption key from probe-ack:', peerId.substring(0, 6));
|
|
736
|
+
} catch (e) {
|
|
737
|
+
console.warn('[Crypto] Failed to parse peer public key from probe-ack:', e?.message);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
634
740
|
// Peer session can legitimately rotate between hello/probe/ack (reloads, relay history).
|
|
635
741
|
// Since this message is already targeted to our toSession, accept it and update our view.
|
|
636
742
|
if (typeof payload.fromSession === 'string' && payload.fromSession.length >= 6) {
|
|
@@ -790,7 +896,17 @@ async function connectNostr() {
|
|
|
790
896
|
log(`Selected relay: ${selected.relayUrl}`, 'success');
|
|
791
897
|
|
|
792
898
|
log(`Joined Nostr room: ${effectiveRoom}`, 'success');
|
|
793
|
-
|
|
899
|
+
// Include unsea public key in hello if encryption is enabled
|
|
900
|
+
const helloPayload = { type: 'hello', session: mySessionNonce };
|
|
901
|
+
if (encryptionEnabled && myKeyPair && myKeyPair.publicKey) {
|
|
902
|
+
try {
|
|
903
|
+
helloPayload.encryptionPublicKey = JSON.stringify(myKeyPair.publicKey);
|
|
904
|
+
console.log('[Crypto] Including public key in hello message');
|
|
905
|
+
} catch (e) {
|
|
906
|
+
console.warn('[Crypto] Failed to serialize public key:', e?.message);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
await nostrClient.send(helloPayload);
|
|
794
910
|
|
|
795
911
|
// Kick any peers we saw while selecting relays.
|
|
796
912
|
for (const peerId of deferredHelloPeers) {
|
|
@@ -929,7 +1045,6 @@ async function connectWebRTC() {
|
|
|
929
1045
|
}
|
|
930
1046
|
|
|
931
1047
|
window.disconnect = function() {
|
|
932
|
-
updateRole(null);
|
|
933
1048
|
if (nostrClient) {
|
|
934
1049
|
nostrClient.disconnect().catch(() => {});
|
|
935
1050
|
nostrClient = null;
|
|
@@ -957,6 +1072,16 @@ window.disconnect = function() {
|
|
|
957
1072
|
}
|
|
958
1073
|
};
|
|
959
1074
|
|
|
1075
|
+
window.toggleEncryption = function() {
|
|
1076
|
+
const checkbox = document.getElementById('encryptionToggle');
|
|
1077
|
+
encryptionEnabled = checkbox.checked;
|
|
1078
|
+
const status = encryptionEnabled ? 'enabled' : 'disabled';
|
|
1079
|
+
console.log('[Crypto] Encryption', status);
|
|
1080
|
+
log(`Signaling encryption ${status}`, 'info');
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
// REMOVED: load handler was firing before HTML existed
|
|
1084
|
+
|
|
960
1085
|
async function createPeerConnection(peerId, shouldInitiate) {
|
|
961
1086
|
if (peerConnections.has(peerId)) {
|
|
962
1087
|
const existing = peerConnections.get(peerId);
|
|
@@ -1060,7 +1185,17 @@ function setupDataChannel(peerId, dataChannel) {
|
|
|
1060
1185
|
};
|
|
1061
1186
|
|
|
1062
1187
|
dataChannel.onmessage = (event) => {
|
|
1063
|
-
|
|
1188
|
+
let message;
|
|
1189
|
+
try {
|
|
1190
|
+
message = JSON.parse(event.data);
|
|
1191
|
+
} catch {
|
|
1192
|
+
// Treat as plain text chat message
|
|
1193
|
+
displayChatMessage(event.data, `${peerId.substring(0, 6)}...`, false);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// Treat as chat message
|
|
1198
|
+
displayChatMessage(JSON.stringify(message), `${peerId.substring(0, 6)}...`, false);
|
|
1064
1199
|
};
|
|
1065
1200
|
|
|
1066
1201
|
dataChannel.onclose = () => {
|