@modularizer/plat 0.9.0 → 0.11.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/dist/client/css-transport-plugin.d.ts +2 -1
- package/dist/client/css-transport-plugin.d.ts.map +1 -1
- package/dist/client/css-transport-plugin.js +2 -2
- package/dist/client/css-transport-plugin.js.map +1 -1
- package/dist/client-entry.d.ts +2 -0
- package/dist/client-entry.d.ts.map +1 -1
- package/dist/client-entry.js +2 -0
- package/dist/client-entry.js.map +1 -1
- package/dist/client-server-entry.d.ts +1 -0
- package/dist/client-server-entry.d.ts.map +1 -1
- package/dist/client-server-entry.js +1 -0
- package/dist/client-server-entry.js.map +1 -1
- package/dist/client-side-python/runtime.d.ts.map +1 -1
- package/dist/client-side-python/runtime.js +8 -0
- package/dist/client-side-python/runtime.js.map +1 -1
- package/dist/client-side-server/channel.d.ts.map +1 -1
- package/dist/client-side-server/channel.js +93 -29
- package/dist/client-side-server/channel.js.map +1 -1
- package/dist/client-side-server/identity.d.ts +80 -17
- package/dist/client-side-server/identity.d.ts.map +1 -1
- package/dist/client-side-server/identity.js +212 -31
- package/dist/client-side-server/identity.js.map +1 -1
- package/dist/client-side-server/mqtt-webrtc.d.ts +43 -6
- package/dist/client-side-server/mqtt-webrtc.d.ts.map +1 -1
- package/dist/client-side-server/mqtt-webrtc.js +853 -68
- package/dist/client-side-server/mqtt-webrtc.js.map +1 -1
- package/dist/client-side-server/plat-fetch.d.ts.map +1 -1
- package/dist/client-side-server/plat-fetch.js +10 -0
- package/dist/client-side-server/plat-fetch.js.map +1 -1
- package/dist/client-side-server/plat-service-worker.js +1 -36
- package/dist/client-side-server/plat-service-worker.js.map +1 -1
- package/dist/client-side-server/protocol.d.ts +68 -3
- package/dist/client-side-server/protocol.d.ts.map +1 -1
- package/dist/client-side-server/protocol.js +110 -0
- package/dist/client-side-server/protocol.js.map +1 -1
- package/dist/client-side-server/runtime.d.ts.map +1 -1
- package/dist/client-side-server/runtime.js +7 -0
- package/dist/client-side-server/runtime.js.map +1 -1
- package/dist/client-side-server/secure-crypto.d.ts +35 -0
- package/dist/client-side-server/secure-crypto.d.ts.map +1 -0
- package/dist/client-side-server/secure-crypto.js +171 -0
- package/dist/client-side-server/secure-crypto.js.map +1 -0
- package/dist/client-side-server/server.d.ts.map +1 -1
- package/dist/client-side-server/server.js +57 -5
- package/dist/client-side-server/server.js.map +1 -1
- package/dist/client-side-server/signaling.d.ts +5 -0
- package/dist/client-side-server/signaling.d.ts.map +1 -1
- package/dist/client-side-server/signaling.js +12 -0
- package/dist/client-side-server/signaling.js.map +1 -1
- package/dist/server/auth/jwt.d.ts.map +1 -1
- package/dist/server/auth/jwt.js +1 -2
- package/dist/server/auth/jwt.js.map +1 -1
- package/dist/server/authority-server.d.ts +4 -4
- package/dist/server/authority-server.d.ts.map +1 -1
- package/dist/server/authority-server.js +23 -21
- package/dist/server/authority-server.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/rpc-protocol-plugin.d.ts.map +1 -1
- package/dist/server/rpc-protocol-plugin.js +11 -1
- package/dist/server/rpc-protocol-plugin.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +4 -2
- package/dist/server/server.js.map +1 -1
- package/dist/server/websocket-plugin.d.ts +14 -0
- package/dist/server/websocket-plugin.d.ts.map +1 -0
- package/dist/server/websocket-plugin.js +93 -0
- package/dist/server/websocket-plugin.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import mqtt from 'mqtt';
|
|
2
2
|
import { createClientSideServerTransportPlugin } from '../client/css-transport-plugin.js';
|
|
3
3
|
import { createRTCDataChannelAdapter } from './channel.js';
|
|
4
|
-
import { buildClientSideServerIdentityChallenge, clientSideServerPublicKeysEqual, getOrCreateClientSideServerIdentityKeyPair, loadTrustedClientSideServerRecordFromMap, resolveTrustedClientSideServerFromAuthorities, loadTrustedClientSideServerRecord, trustClientSideServerFromAuthorityRecord, saveTrustedClientSideServerRecordToMap, signClientSideServerChallenge, toClientSideServerPublicIdentity, trustClientSideServerOnFirstUse, verifyClientSideServerChallenge,
|
|
5
|
-
import { isClientSideServerPeerMessage, } from './protocol.js';
|
|
4
|
+
import { buildClientSideServerIdentityChallenge, clientSideServerPublicKeysEqual, generateClientSideServerEncryptionKeyPair, getOrCreateClientSideServerIdentityKeyPair, isClientSideServerSignedAuthorityRecordV2, isClientSideServerTrustedServerRecordV2, loadTrustedClientSideServerRecordFromMap, resolveTrustedClientSideServerFromAuthorities, loadTrustedClientSideServerRecord, trustClientSideServerFromAuthorityRecord, saveTrustedClientSideServerRecordToMap, signClientSideServerChallenge, toClientSideServerEncryptionPublicIdentity, toClientSideServerPublicIdentity, trustClientSideServerOnFirstUse, verifyClientSideServerChallenge, verifyAnySignedClientSideServerAuthorityRecord, } from './identity.js';
|
|
5
|
+
import { isClientSideServerSealedEnvelope, isClientSideServerSealedPayload, isClientSideServerPeerMessage, } from './protocol.js';
|
|
6
|
+
import { choosePaddingBucket, computeSessionId, decodeBase64Url, decryptJsonAead, deriveAeadKeyFromX25519, encodeBase64Url, encryptJsonAead, generateEphemeralX25519KeyPair, importX25519PrivateKeyJwk, importX25519PublicKeyJwk, padCiphertext, randomNonce12, stableJson, unpadCiphertext, utf8, } from './secure-crypto.js';
|
|
6
7
|
import { parseClientSideServerAddress, } from './signaling.js';
|
|
7
8
|
export const DEFAULT_CLIENT_SIDE_SERVER_MQTT_BROKER = 'wss://broker.emqx.io:8084/mqtt';
|
|
8
9
|
export const DEFAULT_CLIENT_SIDE_SERVER_MQTT_TOPIC = 'mrtchat/plat-css';
|
|
10
|
+
export const DEFAULT_CLIENT_SIDE_SERVER_SEALED_TOPIC = 'plat';
|
|
11
|
+
export const DEFAULT_CLIENT_SIDE_SERVER_MAX_SEALED_MESSAGE_BYTES = 65536;
|
|
12
|
+
export const DEFAULT_CLIENT_SIDE_SERVER_REPLAY_WINDOW_MS = 5 * 60_000;
|
|
13
|
+
export const DEFAULT_CLIENT_SIDE_SERVER_CLOCK_SKEW_TOLERANCE_MS = 30_000;
|
|
9
14
|
export const DEFAULT_CLIENT_SIDE_SERVER_ICE_SERVERS = [
|
|
10
15
|
{ urls: 'stun:stun.l.google.com:19302' },
|
|
11
16
|
{ urls: 'stun:stun1.l.google.com:19302' },
|
|
@@ -26,8 +31,11 @@ export class ClientSideServerMQTTWebRTCServer {
|
|
|
26
31
|
announceTimer;
|
|
27
32
|
identityKeyPair;
|
|
28
33
|
publicIdentity;
|
|
34
|
+
encryptionKeyPair;
|
|
35
|
+
encryptionPublicIdentity;
|
|
29
36
|
clientCount = 0;
|
|
30
37
|
resolvedInstanceInfo;
|
|
38
|
+
seenSealedNonces = new Map();
|
|
31
39
|
constructor(options) {
|
|
32
40
|
this.options = options;
|
|
33
41
|
this.serverInstanceId = `${options.serverName}:${randomId('server')}`;
|
|
@@ -39,11 +47,18 @@ export class ClientSideServerMQTTWebRTCServer {
|
|
|
39
47
|
if (this.mqtt)
|
|
40
48
|
return;
|
|
41
49
|
await this.ensureIdentity();
|
|
50
|
+
if (isSecureSignalingEnabled(this.options)) {
|
|
51
|
+
await this.ensureEncryptionIdentity();
|
|
52
|
+
}
|
|
42
53
|
this.mqtt = await connectToBroker(this.options);
|
|
43
54
|
this.mqtt.on('message', (_topic, payload) => {
|
|
44
55
|
void this.onMessage(payload);
|
|
45
56
|
});
|
|
46
|
-
await subscribe(this.mqtt, this.options
|
|
57
|
+
await subscribe(this.mqtt, resolvePlaintextTopic(this.options));
|
|
58
|
+
const sealedTopic = resolveSealedTopic(this.options);
|
|
59
|
+
if (sealedTopic !== resolvePlaintextTopic(this.options)) {
|
|
60
|
+
await subscribe(this.mqtt, sealedTopic);
|
|
61
|
+
}
|
|
47
62
|
// Compute and cache instance info (includes openapi hash) before first announce
|
|
48
63
|
this.resolvedInstanceInfo = await this.buildResolvedInstanceInfo();
|
|
49
64
|
await this.announce();
|
|
@@ -73,7 +88,7 @@ export class ClientSideServerMQTTWebRTCServer {
|
|
|
73
88
|
async announce() {
|
|
74
89
|
if (!this.mqtt)
|
|
75
90
|
return;
|
|
76
|
-
await
|
|
91
|
+
await publishJson(this.mqtt, resolvePlaintextTopic(this.options), {
|
|
77
92
|
protocol: 'plat-css-v1',
|
|
78
93
|
kind: 'announce',
|
|
79
94
|
senderId: this.serverInstanceId,
|
|
@@ -86,33 +101,80 @@ export class ClientSideServerMQTTWebRTCServer {
|
|
|
86
101
|
}
|
|
87
102
|
async onMessage(payload) {
|
|
88
103
|
const message = parseSignalingMessage(payload);
|
|
89
|
-
if (
|
|
104
|
+
if (message) {
|
|
105
|
+
if (message.senderId === this.serverInstanceId)
|
|
106
|
+
return;
|
|
107
|
+
if (message.targetId && message.targetId !== this.options.serverName && message.targetId !== this.serverInstanceId)
|
|
108
|
+
return;
|
|
109
|
+
if (message.kind === 'discover' && message.targetId === this.options.serverName && message.challengeNonce) {
|
|
110
|
+
await this.respondToDiscover(message);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (!isSecureSignalingEnabled(this.options)) {
|
|
114
|
+
if (message.kind === 'offer'
|
|
115
|
+
&& (message.targetId === this.options.serverName || message.targetId === this.serverInstanceId)
|
|
116
|
+
&& message.connectionId
|
|
117
|
+
&& message.description) {
|
|
118
|
+
await this.acceptOffer(message);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (message.kind === 'ice'
|
|
122
|
+
&& (message.targetId === this.options.serverName || message.targetId === this.serverInstanceId)
|
|
123
|
+
&& message.connectionId
|
|
124
|
+
&& message.candidate) {
|
|
125
|
+
const peer = this.peers.get(message.connectionId);
|
|
126
|
+
if (peer?.remoteDescription) {
|
|
127
|
+
await peer.addIceCandidate(new RTCIceCandidate(message.candidate));
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const pending = this.pendingCandidates.get(message.connectionId) ?? [];
|
|
131
|
+
pending.push(message.candidate);
|
|
132
|
+
this.pendingCandidates.set(message.connectionId, pending);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!isSecureSignalingEnabled(this.options))
|
|
90
139
|
return;
|
|
91
|
-
|
|
140
|
+
const opened = await openClientSideServerSealedEnvelopeForServer({
|
|
141
|
+
envelopePayload: payload,
|
|
142
|
+
serverEncryptionKeyPair: this.encryptionKeyPair,
|
|
143
|
+
});
|
|
144
|
+
if (!opened || envelopeIsReplayedOrStale(opened.payload, this.options))
|
|
92
145
|
return;
|
|
93
|
-
if (
|
|
94
|
-
|
|
146
|
+
if (this.isSealedReplay(opened.envelope.senderId, opened.envelope.nonce)) {
|
|
147
|
+
debugSecure('sealed payload replayed', {
|
|
148
|
+
serverName: this.options.serverName,
|
|
149
|
+
senderId: opened.envelope.senderId,
|
|
150
|
+
nonce: opened.envelope.nonce,
|
|
151
|
+
});
|
|
95
152
|
return;
|
|
96
153
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
154
|
+
this.rememberSealedNonce(opened.envelope.senderId, opened.envelope.nonce);
|
|
155
|
+
debugSecure('sealed payload accepted', {
|
|
156
|
+
serverName: this.options.serverName,
|
|
157
|
+
senderId: opened.envelope.senderId,
|
|
158
|
+
type: opened.payload.type,
|
|
159
|
+
connectionId: opened.payload.connectionId,
|
|
160
|
+
});
|
|
161
|
+
if (opened.payload.type === 'offer') {
|
|
162
|
+
await this.acceptSealedOffer(opened.payload, {
|
|
163
|
+
senderId: opened.envelope.senderId,
|
|
164
|
+
clientEphemeralPublicKeyJwk: opened.envelope.clientEphemeralPublicKeyJwk,
|
|
165
|
+
connectionId: opened.payload.connectionId,
|
|
166
|
+
});
|
|
102
167
|
return;
|
|
103
168
|
}
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
&& message.connectionId
|
|
107
|
-
&& message.candidate) {
|
|
108
|
-
const peer = this.peers.get(message.connectionId);
|
|
169
|
+
if (opened.payload.type === 'ice') {
|
|
170
|
+
const peer = this.peers.get(opened.payload.connectionId);
|
|
109
171
|
if (peer?.remoteDescription) {
|
|
110
|
-
await peer.addIceCandidate(new RTCIceCandidate(
|
|
172
|
+
await peer.addIceCandidate(new RTCIceCandidate(opened.payload.candidate));
|
|
111
173
|
}
|
|
112
174
|
else {
|
|
113
|
-
const pending = this.pendingCandidates.get(
|
|
114
|
-
pending.push(
|
|
115
|
-
this.pendingCandidates.set(
|
|
175
|
+
const pending = this.pendingCandidates.get(opened.payload.connectionId) ?? [];
|
|
176
|
+
pending.push(opened.payload.candidate);
|
|
177
|
+
this.pendingCandidates.set(opened.payload.connectionId, pending);
|
|
116
178
|
}
|
|
117
179
|
}
|
|
118
180
|
}
|
|
@@ -120,19 +182,32 @@ export class ClientSideServerMQTTWebRTCServer {
|
|
|
120
182
|
if (!this.mqtt || !message.challengeNonce)
|
|
121
183
|
return;
|
|
122
184
|
await this.ensureIdentity();
|
|
185
|
+
let encryptionBootstrap = {};
|
|
186
|
+
if (message.requestEncryptionIdentity && message.bootstrapClientEphemeralPublicKeyJwk) {
|
|
187
|
+
await this.ensureEncryptionIdentity();
|
|
188
|
+
encryptionBootstrap = await createPublicEncryptionBootstrapResponse({
|
|
189
|
+
serverInstanceId: this.serverInstanceId,
|
|
190
|
+
serverName: this.options.serverName,
|
|
191
|
+
serverEncryptionKeyPair: this.encryptionKeyPair,
|
|
192
|
+
serverEncryptionIdentity: this.encryptionPublicIdentity,
|
|
193
|
+
challengeNonce: message.challengeNonce,
|
|
194
|
+
clientEphemeralPublicKeyJwk: message.bootstrapClientEphemeralPublicKeyJwk,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
123
197
|
const challengeString = buildClientSideServerIdentityChallenge({
|
|
124
198
|
serverName: this.options.serverName,
|
|
125
199
|
connectionId: this.serverInstanceId,
|
|
126
200
|
challengeNonce: message.challengeNonce,
|
|
127
201
|
});
|
|
128
202
|
const challengeSignature = await signClientSideServerChallenge(this.identityKeyPair, challengeString);
|
|
129
|
-
await
|
|
203
|
+
await publishJson(this.mqtt, resolvePlaintextTopic(this.options), {
|
|
130
204
|
protocol: 'plat-css-v1',
|
|
131
205
|
kind: 'announce',
|
|
132
206
|
senderId: this.serverInstanceId,
|
|
133
207
|
serverName: this.options.serverName,
|
|
134
208
|
identity: this.publicIdentity,
|
|
135
209
|
authorityRecord: this.options.identity?.authorityRecord,
|
|
210
|
+
...encryptionBootstrap,
|
|
136
211
|
challengeNonce: message.challengeNonce,
|
|
137
212
|
challengeSignature,
|
|
138
213
|
workerInfo: this.buildWorkerInfo(),
|
|
@@ -190,12 +265,26 @@ export class ClientSideServerMQTTWebRTCServer {
|
|
|
190
265
|
});
|
|
191
266
|
}
|
|
192
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Send a message to every currently-connected channel. Used to push
|
|
270
|
+
* server-originated events (e.g. "files changed") to subscribers.
|
|
271
|
+
*/
|
|
272
|
+
async broadcast(message) {
|
|
273
|
+
for (const channel of this.channels.values()) {
|
|
274
|
+
try {
|
|
275
|
+
await channel.send(message);
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
console.warn('[plat-css] broadcast to channel failed', err);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
193
282
|
async drain() {
|
|
194
283
|
if (this.options.workerInfo) {
|
|
195
284
|
this.options.workerInfo.acceptingNewClients = false;
|
|
196
285
|
}
|
|
197
286
|
if (this.mqtt) {
|
|
198
|
-
await
|
|
287
|
+
await publishJson(this.mqtt, resolvePlaintextTopic(this.options), {
|
|
199
288
|
protocol: 'plat-css-v1',
|
|
200
289
|
kind: 'announce',
|
|
201
290
|
senderId: this.serverInstanceId,
|
|
@@ -221,7 +310,7 @@ export class ClientSideServerMQTTWebRTCServer {
|
|
|
221
310
|
peer.onicecandidate = (event) => {
|
|
222
311
|
if (!event.candidate || !this.mqtt)
|
|
223
312
|
return;
|
|
224
|
-
void
|
|
313
|
+
void publishJson(this.mqtt, resolvePlaintextTopic(this.options), {
|
|
225
314
|
protocol: 'plat-css-v1',
|
|
226
315
|
kind: 'ice',
|
|
227
316
|
senderId: this.serverInstanceId,
|
|
@@ -265,7 +354,7 @@ export class ClientSideServerMQTTWebRTCServer {
|
|
|
265
354
|
this.pendingCandidates.delete(message.connectionId);
|
|
266
355
|
const answer = await peer.createAnswer();
|
|
267
356
|
await peer.setLocalDescription(answer);
|
|
268
|
-
await
|
|
357
|
+
await publishJson(this.mqtt, resolvePlaintextTopic(this.options), {
|
|
269
358
|
protocol: 'plat-css-v1',
|
|
270
359
|
kind: 'answer',
|
|
271
360
|
senderId: this.serverInstanceId,
|
|
@@ -286,6 +375,113 @@ export class ClientSideServerMQTTWebRTCServer {
|
|
|
286
375
|
at: Date.now(),
|
|
287
376
|
});
|
|
288
377
|
}
|
|
378
|
+
async acceptSealedOffer(payload, response) {
|
|
379
|
+
if (!this.mqtt)
|
|
380
|
+
return;
|
|
381
|
+
await this.ensureIdentity();
|
|
382
|
+
await this.ensureEncryptionIdentity();
|
|
383
|
+
const authOk = await this.authorizeSealedOffer(payload, response);
|
|
384
|
+
if (!authOk)
|
|
385
|
+
return;
|
|
386
|
+
if (this.options.workerInfo?.acceptingNewClients === false) {
|
|
387
|
+
await this.publishSealedResponse(response, {
|
|
388
|
+
type: 'reject',
|
|
389
|
+
connectionId: payload.connectionId,
|
|
390
|
+
serverName: this.options.serverName,
|
|
391
|
+
reason: 'server-not-accepting',
|
|
392
|
+
at: Date.now(),
|
|
393
|
+
});
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const peer = new RTCPeerConnection({
|
|
397
|
+
iceServers: this.options.iceServers ?? DEFAULT_CLIENT_SIDE_SERVER_ICE_SERVERS,
|
|
398
|
+
});
|
|
399
|
+
this.peers.set(payload.connectionId, peer);
|
|
400
|
+
peer.onicecandidate = (event) => {
|
|
401
|
+
if (!event.candidate || !this.mqtt)
|
|
402
|
+
return;
|
|
403
|
+
void this.publishSealedResponse(response, {
|
|
404
|
+
type: 'ice',
|
|
405
|
+
connectionId: payload.connectionId,
|
|
406
|
+
serverName: this.options.serverName,
|
|
407
|
+
candidate: event.candidate.toJSON(),
|
|
408
|
+
at: Date.now(),
|
|
409
|
+
});
|
|
410
|
+
};
|
|
411
|
+
peer.onconnectionstatechange = () => {
|
|
412
|
+
if (peer.connectionState === 'failed' || peer.connectionState === 'closed' || peer.connectionState === 'disconnected') {
|
|
413
|
+
this.unsubscribeByConnection.get(payload.connectionId)?.();
|
|
414
|
+
this.unsubscribeByConnection.delete(payload.connectionId);
|
|
415
|
+
this.peers.delete(payload.connectionId);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
peer.ondatachannel = (event) => {
|
|
419
|
+
const channel = createRTCDataChannelAdapter(event.channel);
|
|
420
|
+
const connId = payload.connectionId;
|
|
421
|
+
const controlUnsubscribe = channel.subscribe(async (msg) => {
|
|
422
|
+
await this.handleControlMessage(msg, channel, connId);
|
|
423
|
+
});
|
|
424
|
+
const serveUnsubscribe = this.options.server.serveChannel(channel);
|
|
425
|
+
this.channels.set(connId, channel);
|
|
426
|
+
this.clientCount++;
|
|
427
|
+
const cleanup = () => {
|
|
428
|
+
controlUnsubscribe();
|
|
429
|
+
serveUnsubscribe();
|
|
430
|
+
this.channels.delete(connId);
|
|
431
|
+
this.clientCount--;
|
|
432
|
+
this.unsubscribeByConnection.delete(connId);
|
|
433
|
+
};
|
|
434
|
+
this.unsubscribeByConnection.set(connId, cleanup);
|
|
435
|
+
event.channel.addEventListener('close', cleanup, { once: true });
|
|
436
|
+
};
|
|
437
|
+
try {
|
|
438
|
+
await peer.setRemoteDescription(new RTCSessionDescription(payload.description));
|
|
439
|
+
for (const candidate of this.pendingCandidates.get(payload.connectionId) ?? []) {
|
|
440
|
+
await peer.addIceCandidate(new RTCIceCandidate(candidate));
|
|
441
|
+
}
|
|
442
|
+
this.pendingCandidates.delete(payload.connectionId);
|
|
443
|
+
const answer = await peer.createAnswer();
|
|
444
|
+
await peer.setLocalDescription(answer);
|
|
445
|
+
await this.publishSealedResponse(response, {
|
|
446
|
+
type: 'answer',
|
|
447
|
+
connectionId: payload.connectionId,
|
|
448
|
+
serverName: this.options.serverName,
|
|
449
|
+
description: answer,
|
|
450
|
+
identity: this.publicIdentity,
|
|
451
|
+
authorityRecord: this.options.identity?.authorityRecord,
|
|
452
|
+
challengeNonce: payload.challengeNonce,
|
|
453
|
+
challengeSignature: payload.challengeNonce
|
|
454
|
+
? await signClientSideServerChallenge(this.identityKeyPair, buildClientSideServerIdentityChallenge({
|
|
455
|
+
serverName: this.options.serverName,
|
|
456
|
+
connectionId: payload.connectionId,
|
|
457
|
+
challengeNonce: payload.challengeNonce,
|
|
458
|
+
}))
|
|
459
|
+
: undefined,
|
|
460
|
+
at: Date.now(),
|
|
461
|
+
});
|
|
462
|
+
debugSecure('secure answer sent', {
|
|
463
|
+
serverName: this.options.serverName,
|
|
464
|
+
connectionId: payload.connectionId,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
debugSecure('sealed offer processing failed', {
|
|
469
|
+
serverName: this.options.serverName,
|
|
470
|
+
connectionId: payload.connectionId,
|
|
471
|
+
error: error instanceof Error ? error.message : String(error),
|
|
472
|
+
});
|
|
473
|
+
await this.publishSealedResponse(response, {
|
|
474
|
+
type: 'reject',
|
|
475
|
+
connectionId: payload.connectionId,
|
|
476
|
+
serverName: this.options.serverName,
|
|
477
|
+
reason: 'bad-message',
|
|
478
|
+
at: Date.now(),
|
|
479
|
+
});
|
|
480
|
+
peer.close();
|
|
481
|
+
this.peers.delete(payload.connectionId);
|
|
482
|
+
this.pendingCandidates.delete(payload.connectionId);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
289
485
|
async ensureIdentity() {
|
|
290
486
|
if (this.identityKeyPair && this.publicIdentity)
|
|
291
487
|
return;
|
|
@@ -297,6 +493,105 @@ export class ClientSideServerMQTTWebRTCServer {
|
|
|
297
493
|
});
|
|
298
494
|
this.publicIdentity = await toClientSideServerPublicIdentity(this.identityKeyPair);
|
|
299
495
|
}
|
|
496
|
+
async ensureEncryptionIdentity() {
|
|
497
|
+
if (this.encryptionKeyPair && this.encryptionPublicIdentity)
|
|
498
|
+
return;
|
|
499
|
+
if (this.options.serverEncryptionKeyPair) {
|
|
500
|
+
this.encryptionKeyPair = this.options.serverEncryptionKeyPair;
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
this.encryptionKeyPair = await generateClientSideServerEncryptionKeyPair();
|
|
504
|
+
}
|
|
505
|
+
this.encryptionPublicIdentity = await toClientSideServerEncryptionPublicIdentity(this.encryptionKeyPair);
|
|
506
|
+
}
|
|
507
|
+
isSealedReplay(senderId, nonce) {
|
|
508
|
+
this.pruneSeenSealedNonces();
|
|
509
|
+
return this.seenSealedNonces.has(`${senderId}:${nonce}`);
|
|
510
|
+
}
|
|
511
|
+
rememberSealedNonce(senderId, nonce) {
|
|
512
|
+
this.pruneSeenSealedNonces();
|
|
513
|
+
this.seenSealedNonces.set(`${senderId}:${nonce}`, Date.now());
|
|
514
|
+
}
|
|
515
|
+
pruneSeenSealedNonces() {
|
|
516
|
+
const cutoff = Date.now() - (this.options.replayWindowMs ?? DEFAULT_CLIENT_SIDE_SERVER_REPLAY_WINDOW_MS);
|
|
517
|
+
for (const [key, value] of this.seenSealedNonces.entries()) {
|
|
518
|
+
if (value < cutoff) {
|
|
519
|
+
this.seenSealedNonces.delete(key);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async publishSealedResponse(response, payload) {
|
|
524
|
+
if (!this.mqtt)
|
|
525
|
+
return;
|
|
526
|
+
const sealed = await sealClientSideServerPayloadForServer({
|
|
527
|
+
senderId: this.serverInstanceId,
|
|
528
|
+
payload,
|
|
529
|
+
serverEncryptionKeyPair: this.encryptionKeyPair,
|
|
530
|
+
clientEphemeralPublicKeyJwk: response.clientEphemeralPublicKeyJwk,
|
|
531
|
+
maxSealedMessageBytes: this.options.maxSealedMessageBytes,
|
|
532
|
+
});
|
|
533
|
+
if (payload.type === 'reject') {
|
|
534
|
+
debugSecure('sealed reject sent', {
|
|
535
|
+
serverName: this.options.serverName,
|
|
536
|
+
connectionId: payload.connectionId,
|
|
537
|
+
reason: payload.reason,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
await publishRaw(this.mqtt, resolveSealedTopic(this.options), serializeSealedEnvelope(sealed.envelope));
|
|
541
|
+
}
|
|
542
|
+
async authorizeSealedOffer(payload, response) {
|
|
543
|
+
const signalingAuth = this.options.signalingAuth;
|
|
544
|
+
if (!signalingAuth)
|
|
545
|
+
return true;
|
|
546
|
+
if (!payload.auth) {
|
|
547
|
+
if (signalingAuth.required !== false) {
|
|
548
|
+
await this.publishSealedResponse(response, {
|
|
549
|
+
type: 'reject',
|
|
550
|
+
connectionId: payload.connectionId,
|
|
551
|
+
serverName: this.options.serverName,
|
|
552
|
+
reason: 'auth-required',
|
|
553
|
+
at: Date.now(),
|
|
554
|
+
});
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
if (typeof signalingAuth.verify !== 'function') {
|
|
560
|
+
if (signalingAuth.required !== false) {
|
|
561
|
+
await this.publishSealedResponse(response, {
|
|
562
|
+
type: 'reject',
|
|
563
|
+
connectionId: payload.connectionId,
|
|
564
|
+
serverName: this.options.serverName,
|
|
565
|
+
reason: 'auth-required',
|
|
566
|
+
at: Date.now(),
|
|
567
|
+
});
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
let verified = false;
|
|
573
|
+
try {
|
|
574
|
+
verified = await signalingAuth.verify(payload.auth, {
|
|
575
|
+
serverName: this.options.serverName,
|
|
576
|
+
connectionId: payload.connectionId,
|
|
577
|
+
senderId: response.senderId,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
verified = false;
|
|
582
|
+
}
|
|
583
|
+
if (!verified) {
|
|
584
|
+
await this.publishSealedResponse(response, {
|
|
585
|
+
type: 'reject',
|
|
586
|
+
connectionId: payload.connectionId,
|
|
587
|
+
serverName: this.options.serverName,
|
|
588
|
+
reason: 'auth-failed',
|
|
589
|
+
at: Date.now(),
|
|
590
|
+
});
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
300
595
|
}
|
|
301
596
|
export function createClientSideServerMQTTWebRTCTransportPlugin(options = {}) {
|
|
302
597
|
if (isDefaultMQTTWebRTCOptions(options)) {
|
|
@@ -390,7 +685,7 @@ function createClientSideServerMQTTWebRTCPeerPoolInternal(options) {
|
|
|
390
685
|
}
|
|
391
686
|
export async function discoverClientSideServers(serverName, options = {}) {
|
|
392
687
|
const mqttClient = await connectToBroker(options);
|
|
393
|
-
const topic = options
|
|
688
|
+
const topic = resolvePlaintextTopic(options);
|
|
394
689
|
const peerId = `${options.clientIdPrefix ?? 'client'}:${randomId('discover')}`;
|
|
395
690
|
const challengeNonce = randomId('challenge') + '-mqtt';
|
|
396
691
|
const timeoutMs = options.workerPool?.discoveryTimeoutMs ?? 3000;
|
|
@@ -416,7 +711,7 @@ export async function discoverClientSideServers(serverName, options = {}) {
|
|
|
416
711
|
mqttChallengeVerified = await verifyClientSideServerChallenge(message.identity.publicKeyJwk, challengeString, message.challengeSignature);
|
|
417
712
|
}
|
|
418
713
|
if (message.authorityRecord && options.identity?.authority?.publicKeyJwk) {
|
|
419
|
-
const authorityOk = await
|
|
714
|
+
const authorityOk = await verifyAnySignedClientSideServerAuthorityRecord(message.authorityRecord, options.identity.authority.publicKeyJwk);
|
|
420
715
|
if (!authorityOk)
|
|
421
716
|
return;
|
|
422
717
|
}
|
|
@@ -502,7 +797,8 @@ export async function connectFirstDiscovery(address, options) {
|
|
|
502
797
|
export async function createClientSideServerMQTTWebRTCPeerSession(address, options, targetInstanceId) {
|
|
503
798
|
const webrtc = await resolveClientWebRTCImplementation();
|
|
504
799
|
const mqtt = await connectToBroker(options);
|
|
505
|
-
const
|
|
800
|
+
const plaintextTopic = resolvePlaintextTopic(options);
|
|
801
|
+
const sealedTopic = resolveSealedTopic(options);
|
|
506
802
|
const peerId = `${options.clientIdPrefix ?? 'client'}:${randomId('peer')}`;
|
|
507
803
|
const connectionId = randomId('conn');
|
|
508
804
|
const challengeNonce = randomId('challenge');
|
|
@@ -515,11 +811,69 @@ export async function createClientSideServerMQTTWebRTCPeerSession(address, optio
|
|
|
515
811
|
const cleanupCallbacks = [];
|
|
516
812
|
const pendingCandidates = [];
|
|
517
813
|
const expectedIdentity = await resolveExpectedServerIdentity(address.serverName, options.identity);
|
|
814
|
+
let secureState;
|
|
518
815
|
let open = true;
|
|
519
816
|
let answerApplied = false;
|
|
520
817
|
let serverIdentity;
|
|
521
|
-
await subscribe(mqtt,
|
|
818
|
+
await subscribe(mqtt, plaintextTopic);
|
|
819
|
+
if (sealedTopic !== plaintextTopic) {
|
|
820
|
+
await subscribe(mqtt, sealedTopic);
|
|
821
|
+
}
|
|
822
|
+
if (isSecureSignalingEnabled(options)) {
|
|
823
|
+
secureState = await createSecureClientSessionState({
|
|
824
|
+
mqtt,
|
|
825
|
+
address,
|
|
826
|
+
options,
|
|
827
|
+
peerId,
|
|
828
|
+
expectedIdentity,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
522
831
|
const onMessage = async (_topic, payload) => {
|
|
832
|
+
if (secureState) {
|
|
833
|
+
const opened = await openClientSideServerSealedEnvelopeForClient({
|
|
834
|
+
envelopePayload: payload,
|
|
835
|
+
secureState,
|
|
836
|
+
});
|
|
837
|
+
if (!opened || opened.envelope.senderId === peerId || opened.payload.connectionId !== connectionId)
|
|
838
|
+
return;
|
|
839
|
+
if (opened.payload.type === 'reject') {
|
|
840
|
+
ready.reject(new Error(`Client-side server ${address.serverName} rejected connection: ${opened.payload.reason}`));
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (opened.payload.type === 'answer' && opened.payload.description) {
|
|
844
|
+
if (answerApplied)
|
|
845
|
+
return;
|
|
846
|
+
answerApplied = true;
|
|
847
|
+
await verifyServerIdentityForSealedAnswer({
|
|
848
|
+
serverName: address.serverName,
|
|
849
|
+
connectionId,
|
|
850
|
+
challengeNonce,
|
|
851
|
+
message: opened.payload,
|
|
852
|
+
optionsIdentity: options.identity,
|
|
853
|
+
expectedIdentity,
|
|
854
|
+
});
|
|
855
|
+
serverIdentity = opened.payload.identity;
|
|
856
|
+
await peer.setRemoteDescription(webrtc.createSessionDescription(opened.payload.description));
|
|
857
|
+
await maybeTrustServerAfterAnswer(address.serverName, expectedIdentity, opened.payload, options.identity);
|
|
858
|
+
for (const candidate of pendingCandidates.splice(0, pendingCandidates.length)) {
|
|
859
|
+
await peer.addIceCandidate(webrtc.createIceCandidate(candidate));
|
|
860
|
+
}
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
if (opened.payload.type === 'ice') {
|
|
864
|
+
debugSecure('secure ICE received', {
|
|
865
|
+
serverName: address.serverName,
|
|
866
|
+
connectionId,
|
|
867
|
+
});
|
|
868
|
+
if (peer.remoteDescription) {
|
|
869
|
+
await peer.addIceCandidate(webrtc.createIceCandidate(opened.payload.candidate));
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
pendingCandidates.push(opened.payload.candidate);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
523
877
|
const message = parseSignalingMessage(payload);
|
|
524
878
|
if (!message || message.senderId === peerId || message.targetId !== peerId || message.connectionId !== connectionId) {
|
|
525
879
|
return;
|
|
@@ -538,24 +892,7 @@ export async function createClientSideServerMQTTWebRTCPeerSession(address, optio
|
|
|
538
892
|
});
|
|
539
893
|
serverIdentity = message.identity;
|
|
540
894
|
await peer.setRemoteDescription(webrtc.createSessionDescription(message.description));
|
|
541
|
-
|
|
542
|
-
const trusted = await trustClientSideServerFromAuthorityRecord(message.authorityRecord, options.identity.authority.publicKeyJwk, {
|
|
543
|
-
storage: options.identity?.knownHostsStorage,
|
|
544
|
-
storageKey: options.identity?.knownHostsStorageKey,
|
|
545
|
-
});
|
|
546
|
-
if (options.identity?.knownHosts) {
|
|
547
|
-
saveTrustedClientSideServerRecordToMap(options.identity.knownHosts, trusted);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
else if (!expectedIdentity && options.identity?.trustOnFirstUse !== false && message.identity) {
|
|
551
|
-
const trusted = await trustClientSideServerOnFirstUse(address.serverName, message.identity, {
|
|
552
|
-
storage: options.identity?.knownHostsStorage,
|
|
553
|
-
storageKey: options.identity?.knownHostsStorageKey,
|
|
554
|
-
});
|
|
555
|
-
if (options.identity?.knownHosts) {
|
|
556
|
-
saveTrustedClientSideServerRecordToMap(options.identity.knownHosts, trusted);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
895
|
+
await maybeTrustServerAfterAnswer(address.serverName, expectedIdentity, message, options.identity);
|
|
559
896
|
for (const candidate of pendingCandidates.splice(0, pendingCandidates.length)) {
|
|
560
897
|
await peer.addIceCandidate(webrtc.createIceCandidate(candidate));
|
|
561
898
|
}
|
|
@@ -573,10 +910,29 @@ export async function createClientSideServerMQTTWebRTCPeerSession(address, optio
|
|
|
573
910
|
mqtt.on('message', onMessage);
|
|
574
911
|
cleanupCallbacks.push(() => mqtt.off('message', onMessage));
|
|
575
912
|
const offerTarget = targetInstanceId ?? address.serverName;
|
|
913
|
+
const addressAuth = resolveAddressAuth(address);
|
|
576
914
|
peer.onicecandidate = (event) => {
|
|
577
915
|
if (!event.candidate)
|
|
578
916
|
return;
|
|
579
|
-
|
|
917
|
+
if (secureState) {
|
|
918
|
+
void publishSealedClientPayload({
|
|
919
|
+
mqtt,
|
|
920
|
+
options,
|
|
921
|
+
senderId: peerId,
|
|
922
|
+
secureState,
|
|
923
|
+
payload: {
|
|
924
|
+
type: 'ice',
|
|
925
|
+
connectionId,
|
|
926
|
+
serverName: address.serverName,
|
|
927
|
+
candidate: webrtc.serializeIceCandidate(event.candidate),
|
|
928
|
+
at: Date.now(),
|
|
929
|
+
},
|
|
930
|
+
}).then(() => {
|
|
931
|
+
debugSecure('secure ICE sent', { serverName: address.serverName, connectionId });
|
|
932
|
+
});
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
void publish(mqtt, plaintextTopic, {
|
|
580
936
|
protocol: 'plat-css-v1',
|
|
581
937
|
kind: 'ice',
|
|
582
938
|
senderId: peerId,
|
|
@@ -602,22 +958,52 @@ export async function createClientSideServerMQTTWebRTCPeerSession(address, optio
|
|
|
602
958
|
}, { once: true });
|
|
603
959
|
const offer = await peer.createOffer();
|
|
604
960
|
await peer.setLocalDescription(offer);
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
961
|
+
if (secureState) {
|
|
962
|
+
await publishSealedClientPayload({
|
|
963
|
+
mqtt,
|
|
964
|
+
options,
|
|
965
|
+
senderId: peerId,
|
|
966
|
+
secureState,
|
|
967
|
+
payload: {
|
|
968
|
+
type: 'offer',
|
|
969
|
+
connectionId,
|
|
970
|
+
serverName: address.serverName,
|
|
971
|
+
description: offer,
|
|
972
|
+
challengeNonce,
|
|
973
|
+
requirePrivateChallenge: options.requirePrivateChallenge,
|
|
974
|
+
auth: addressAuth,
|
|
975
|
+
at: Date.now(),
|
|
976
|
+
},
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
else {
|
|
980
|
+
await publish(mqtt, plaintextTopic, {
|
|
981
|
+
protocol: 'plat-css-v1',
|
|
982
|
+
kind: 'offer',
|
|
983
|
+
senderId: peerId,
|
|
984
|
+
targetId: offerTarget,
|
|
985
|
+
serverName: address.serverName,
|
|
986
|
+
connectionId,
|
|
987
|
+
description: offer,
|
|
988
|
+
challengeNonce,
|
|
989
|
+
at: Date.now(),
|
|
990
|
+
});
|
|
991
|
+
}
|
|
616
992
|
const timer = setTimeout(() => {
|
|
617
993
|
ready.reject(new Error(`Timed out connecting to client-side server ${address.serverName}`));
|
|
618
994
|
}, timeoutMs);
|
|
619
995
|
cleanupCallbacks.push(() => clearTimeout(timer));
|
|
620
|
-
|
|
996
|
+
try {
|
|
997
|
+
await ready.promise;
|
|
998
|
+
}
|
|
999
|
+
catch (error) {
|
|
1000
|
+
open = false;
|
|
1001
|
+
for (const cleanup of cleanupCallbacks)
|
|
1002
|
+
cleanup();
|
|
1003
|
+
peer.close();
|
|
1004
|
+
await endClient(mqtt);
|
|
1005
|
+
throw error;
|
|
1006
|
+
}
|
|
621
1007
|
const channel = createRTCDataChannelAdapter(dataChannel);
|
|
622
1008
|
const originalClose = channel.close?.bind(channel);
|
|
623
1009
|
return {
|
|
@@ -659,7 +1045,7 @@ export async function createClientSideServerMQTTWebRTCPeerSession(address, optio
|
|
|
659
1045
|
async function resolveExpectedServerIdentity(serverName, options) {
|
|
660
1046
|
const mapped = loadTrustedClientSideServerRecordFromMap(options?.knownHosts, serverName);
|
|
661
1047
|
if (mapped)
|
|
662
|
-
return mapped;
|
|
1048
|
+
return normalizeExpectedIdentity(mapped);
|
|
663
1049
|
if (options?.authorityServers?.length) {
|
|
664
1050
|
const trusted = await resolveTrustedClientSideServerFromAuthorities(serverName, options.authorityServers, {
|
|
665
1051
|
storage: options?.knownHostsStorage,
|
|
@@ -669,12 +1055,12 @@ async function resolveExpectedServerIdentity(serverName, options) {
|
|
|
669
1055
|
if (options.knownHosts) {
|
|
670
1056
|
saveTrustedClientSideServerRecordToMap(options.knownHosts, trusted);
|
|
671
1057
|
}
|
|
672
|
-
return trusted;
|
|
1058
|
+
return normalizeExpectedIdentity(trusted);
|
|
673
1059
|
}
|
|
674
1060
|
}
|
|
675
1061
|
const authorityResolved = options?.authorityResolver ? await options.authorityResolver(serverName) : null;
|
|
676
1062
|
if (authorityResolved)
|
|
677
|
-
return authorityResolved;
|
|
1063
|
+
return normalizeExpectedIdentity(authorityResolved);
|
|
678
1064
|
const stored = loadTrustedClientSideServerRecord(serverName, {
|
|
679
1065
|
storage: options?.knownHostsStorage,
|
|
680
1066
|
storageKey: options?.knownHostsStorageKey,
|
|
@@ -682,7 +1068,7 @@ async function resolveExpectedServerIdentity(serverName, options) {
|
|
|
682
1068
|
if (stored && options?.knownHosts) {
|
|
683
1069
|
saveTrustedClientSideServerRecordToMap(options.knownHosts, stored);
|
|
684
1070
|
}
|
|
685
|
-
return stored;
|
|
1071
|
+
return stored ? normalizeExpectedIdentity(stored) : null;
|
|
686
1072
|
}
|
|
687
1073
|
async function verifyServerIdentityForAnswer(input) {
|
|
688
1074
|
const { message, serverName, connectionId, challengeNonce, expectedIdentity } = input;
|
|
@@ -693,11 +1079,11 @@ async function verifyServerIdentityForAnswer(input) {
|
|
|
693
1079
|
return;
|
|
694
1080
|
}
|
|
695
1081
|
if (message.authorityRecord && input.optionsIdentity?.authority?.publicKeyJwk) {
|
|
696
|
-
const authorityOk = await
|
|
1082
|
+
const authorityOk = await verifyAnySignedClientSideServerAuthorityRecord(message.authorityRecord, input.optionsIdentity.authority.publicKeyJwk);
|
|
697
1083
|
if (!authorityOk) {
|
|
698
1084
|
throw new Error(`Server ${serverName} provided an invalid authority record`);
|
|
699
1085
|
}
|
|
700
|
-
if (!clientSideServerPublicKeysEqual(message.authorityRecord
|
|
1086
|
+
if (!clientSideServerPublicKeysEqual(getAuthoritySigningPublicKey(message.authorityRecord), message.identity.publicKeyJwk)) {
|
|
701
1087
|
throw new Error(`Server ${serverName} authority record does not match presented identity`);
|
|
702
1088
|
}
|
|
703
1089
|
}
|
|
@@ -709,10 +1095,52 @@ async function verifyServerIdentityForAnswer(input) {
|
|
|
709
1095
|
if (!verified) {
|
|
710
1096
|
throw new Error(`Server ${serverName} failed identity challenge verification`);
|
|
711
1097
|
}
|
|
712
|
-
if (expectedIdentity && !clientSideServerPublicKeysEqual(expectedIdentity.publicKeyJwk, message.identity.publicKeyJwk)) {
|
|
1098
|
+
if (expectedIdentity && !clientSideServerPublicKeysEqual(expectedIdentity.signing.publicKeyJwk, message.identity.publicKeyJwk)) {
|
|
713
1099
|
throw new Error(`Server ${serverName} presented an unexpected public key`);
|
|
714
1100
|
}
|
|
715
1101
|
}
|
|
1102
|
+
async function verifyServerIdentityForSealedAnswer(input) {
|
|
1103
|
+
const message = {
|
|
1104
|
+
protocol: 'plat-css-v1',
|
|
1105
|
+
kind: 'answer',
|
|
1106
|
+
senderId: 'sealed',
|
|
1107
|
+
connectionId: input.connectionId,
|
|
1108
|
+
serverName: input.serverName,
|
|
1109
|
+
description: input.message.description,
|
|
1110
|
+
identity: input.message.identity,
|
|
1111
|
+
authorityRecord: input.message.authorityRecord,
|
|
1112
|
+
challengeNonce: input.message.challengeNonce,
|
|
1113
|
+
challengeSignature: input.message.challengeSignature,
|
|
1114
|
+
at: input.message.at,
|
|
1115
|
+
};
|
|
1116
|
+
await verifyServerIdentityForAnswer({
|
|
1117
|
+
...input,
|
|
1118
|
+
message,
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
async function maybeTrustServerAfterAnswer(serverName, expectedIdentity, message, identityOptions) {
|
|
1122
|
+
if (expectedIdentity)
|
|
1123
|
+
return;
|
|
1124
|
+
if (message.authorityRecord && identityOptions?.authority?.publicKeyJwk) {
|
|
1125
|
+
const trusted = await trustClientSideServerFromAuthorityRecord(message.authorityRecord, identityOptions.authority.publicKeyJwk, {
|
|
1126
|
+
storage: identityOptions?.knownHostsStorage,
|
|
1127
|
+
storageKey: identityOptions?.knownHostsStorageKey,
|
|
1128
|
+
});
|
|
1129
|
+
if (identityOptions?.knownHosts) {
|
|
1130
|
+
saveTrustedClientSideServerRecordToMap(identityOptions.knownHosts, trusted);
|
|
1131
|
+
}
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (identityOptions?.trustOnFirstUse !== false && message.identity) {
|
|
1135
|
+
const trusted = await trustClientSideServerOnFirstUse(serverName, message.identity, {
|
|
1136
|
+
storage: identityOptions?.knownHostsStorage,
|
|
1137
|
+
storageKey: identityOptions?.knownHostsStorageKey,
|
|
1138
|
+
});
|
|
1139
|
+
if (identityOptions?.knownHosts) {
|
|
1140
|
+
saveTrustedClientSideServerRecordToMap(identityOptions.knownHosts, trusted);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
716
1144
|
export function createClientSideServerMQTTWebRTCServer(options) {
|
|
717
1145
|
return new ClientSideServerMQTTWebRTCServer(options);
|
|
718
1146
|
}
|
|
@@ -763,6 +1191,19 @@ function publish(client, topic, message) {
|
|
|
763
1191
|
});
|
|
764
1192
|
});
|
|
765
1193
|
}
|
|
1194
|
+
function publishJson(client, topic, message) {
|
|
1195
|
+
return publish(client, topic, message);
|
|
1196
|
+
}
|
|
1197
|
+
function publishRaw(client, topic, message) {
|
|
1198
|
+
return new Promise((resolve, reject) => {
|
|
1199
|
+
client.publish(topic, message, (error) => {
|
|
1200
|
+
if (error)
|
|
1201
|
+
reject(error);
|
|
1202
|
+
else
|
|
1203
|
+
resolve();
|
|
1204
|
+
});
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
766
1207
|
function endClient(client) {
|
|
767
1208
|
return new Promise((resolve) => {
|
|
768
1209
|
client.end(false, {}, () => resolve());
|
|
@@ -790,6 +1231,350 @@ function randomId(prefix) {
|
|
|
790
1231
|
function isDefaultMQTTWebRTCOptions(options) {
|
|
791
1232
|
return Object.keys(options).length === 0;
|
|
792
1233
|
}
|
|
1234
|
+
function isSecureSignalingEnabled(options) {
|
|
1235
|
+
return options.secureSignaling !== false;
|
|
1236
|
+
}
|
|
1237
|
+
function resolvePlaintextTopic(options) {
|
|
1238
|
+
return options.mqttTopic ?? DEFAULT_CLIENT_SIDE_SERVER_MQTT_TOPIC;
|
|
1239
|
+
}
|
|
1240
|
+
function resolveSealedTopic(options) {
|
|
1241
|
+
if (isSecureSignalingEnabled(options) && options.anonymousRouting !== false) {
|
|
1242
|
+
return options.sealedTopic ?? DEFAULT_CLIENT_SIDE_SERVER_SEALED_TOPIC;
|
|
1243
|
+
}
|
|
1244
|
+
return resolvePlaintextTopic(options);
|
|
1245
|
+
}
|
|
1246
|
+
function envelopeIsReplayedOrStale(payload, options) {
|
|
1247
|
+
const replayWindowMs = options.replayWindowMs ?? DEFAULT_CLIENT_SIDE_SERVER_REPLAY_WINDOW_MS;
|
|
1248
|
+
const clockSkewToleranceMs = options.clockSkewToleranceMs ?? DEFAULT_CLIENT_SIDE_SERVER_CLOCK_SKEW_TOLERANCE_MS;
|
|
1249
|
+
const stale = Math.abs(Date.now() - payload.at) > (clockSkewToleranceMs + replayWindowMs);
|
|
1250
|
+
if (stale) {
|
|
1251
|
+
debugSecure('sealed payload stale', { type: payload.type, connectionId: payload.connectionId, at: payload.at });
|
|
1252
|
+
}
|
|
1253
|
+
return stale;
|
|
1254
|
+
}
|
|
1255
|
+
async function openClientSideServerSealedEnvelopeForServer(input) {
|
|
1256
|
+
if (!input.serverEncryptionKeyPair)
|
|
1257
|
+
return null;
|
|
1258
|
+
const envelope = parseSealedEnvelope(input.envelopePayload);
|
|
1259
|
+
if (!envelope)
|
|
1260
|
+
return null;
|
|
1261
|
+
try {
|
|
1262
|
+
const privateKey = await importX25519PrivateKeyJwk(input.serverEncryptionKeyPair.privateKeyJwk);
|
|
1263
|
+
const publicKey = await importX25519PublicKeyJwk(envelope.clientEphemeralPublicKeyJwk);
|
|
1264
|
+
const aeadKey = await deriveAeadKeyFromX25519(privateKey, publicKey, utf8('plat-css-sealed-signaling-v1'));
|
|
1265
|
+
const ciphertext = unpadCiphertext(decodeBase64Url(envelope.ciphertext));
|
|
1266
|
+
const payload = await decryptJsonAead(aeadKey, ciphertext, buildSealedEnvelopeAad(envelope), decodeBase64Url(envelope.nonce));
|
|
1267
|
+
if (!isClientSideServerSealedPayload(payload)) {
|
|
1268
|
+
debugSecure('sealed decrypt failed', { reason: 'payload-validator' });
|
|
1269
|
+
return null;
|
|
1270
|
+
}
|
|
1271
|
+
return { envelope, payload };
|
|
1272
|
+
}
|
|
1273
|
+
catch (error) {
|
|
1274
|
+
debugSecure('sealed decrypt failed', { error: error instanceof Error ? error.message : String(error) });
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
async function openClientSideServerSealedEnvelopeForClient(input) {
|
|
1279
|
+
const envelope = parseSealedEnvelope(input.envelopePayload);
|
|
1280
|
+
if (!envelope)
|
|
1281
|
+
return null;
|
|
1282
|
+
try {
|
|
1283
|
+
const aeadKey = await deriveAeadKeyFromX25519(input.secureState.keyPair.privateKey, input.secureState.serverEncryptionPublicKey, utf8('plat-css-sealed-signaling-v1'));
|
|
1284
|
+
const payload = await decryptJsonAead(aeadKey, unpadCiphertext(decodeBase64Url(envelope.ciphertext)), buildSealedEnvelopeAad(envelope), decodeBase64Url(envelope.nonce));
|
|
1285
|
+
if (!isClientSideServerSealedPayload(payload))
|
|
1286
|
+
return null;
|
|
1287
|
+
return { envelope, payload };
|
|
1288
|
+
}
|
|
1289
|
+
catch {
|
|
1290
|
+
return null;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
async function createSecureClientSessionState(input) {
|
|
1294
|
+
const keyPair = await generateEphemeralX25519KeyPair();
|
|
1295
|
+
const serverEncryptionPublicKeyJwk = input.address.encryptionPublicKeyJwk
|
|
1296
|
+
?? input.expectedIdentity?.encryption?.publicKeyJwk;
|
|
1297
|
+
if (serverEncryptionPublicKeyJwk) {
|
|
1298
|
+
return {
|
|
1299
|
+
keyPair,
|
|
1300
|
+
serverEncryptionPublicKeyJwk,
|
|
1301
|
+
serverEncryptionPublicKey: await importX25519PublicKeyJwk(serverEncryptionPublicKeyJwk),
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
return bootstrapServerEncryptionIdentity({
|
|
1305
|
+
...input,
|
|
1306
|
+
keyPair,
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
async function bootstrapServerEncryptionIdentity(input) {
|
|
1310
|
+
const challengeNonce = randomId('enc-bootstrap');
|
|
1311
|
+
const timeoutMs = Math.min(input.options.connectionTimeoutMs ?? 10_000, input.options.workerPool?.discoveryTimeoutMs ?? 3_000);
|
|
1312
|
+
const ready = deferred();
|
|
1313
|
+
const onMessage = async (_topic, payload) => {
|
|
1314
|
+
const message = parseSignalingMessage(payload);
|
|
1315
|
+
if (!message || message.senderId === input.peerId)
|
|
1316
|
+
return;
|
|
1317
|
+
if (message.kind !== 'announce')
|
|
1318
|
+
return;
|
|
1319
|
+
if (message.serverName !== input.address.serverName)
|
|
1320
|
+
return;
|
|
1321
|
+
if (message.challengeNonce !== challengeNonce)
|
|
1322
|
+
return;
|
|
1323
|
+
if (!message.identity || !message.challengeSignature || !message.encryptionIdentity)
|
|
1324
|
+
return;
|
|
1325
|
+
if (!message.encryptionChallengeCiphertext || !message.encryptionChallengeNonce)
|
|
1326
|
+
return;
|
|
1327
|
+
if (input.expectedIdentity
|
|
1328
|
+
&& !clientSideServerPublicKeysEqual(input.expectedIdentity.signing.publicKeyJwk, message.identity.publicKeyJwk)) {
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
if (message.authorityRecord && input.options.identity?.authority?.publicKeyJwk) {
|
|
1332
|
+
const authorityOk = await verifyAnySignedClientSideServerAuthorityRecord(message.authorityRecord, input.options.identity.authority.publicKeyJwk);
|
|
1333
|
+
if (!authorityOk)
|
|
1334
|
+
return;
|
|
1335
|
+
if (isClientSideServerSignedAuthorityRecordV2(message.authorityRecord)
|
|
1336
|
+
&& !clientSideServerPublicKeysEqual(message.authorityRecord.encryptionPublicKeyJwk, message.encryptionIdentity.publicKeyJwk)) {
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
const signedChallengeOk = await verifyClientSideServerChallenge(message.identity.publicKeyJwk, buildClientSideServerIdentityChallenge({
|
|
1341
|
+
serverName: input.address.serverName,
|
|
1342
|
+
connectionId: message.senderId,
|
|
1343
|
+
challengeNonce,
|
|
1344
|
+
}), message.challengeSignature);
|
|
1345
|
+
if (!signedChallengeOk)
|
|
1346
|
+
return;
|
|
1347
|
+
try {
|
|
1348
|
+
const serverEncryptionPublicKey = await importX25519PublicKeyJwk(message.encryptionIdentity.publicKeyJwk);
|
|
1349
|
+
const aeadKey = await deriveAeadKeyFromX25519(input.keyPair.privateKey, serverEncryptionPublicKey, utf8('plat-css-sealed-signaling-v1'));
|
|
1350
|
+
const proof = await decryptJsonAead(aeadKey, decodeBase64Url(message.encryptionChallengeCiphertext), buildBootstrapEncryptionAad({
|
|
1351
|
+
senderId: message.senderId,
|
|
1352
|
+
serverName: input.address.serverName,
|
|
1353
|
+
challengeNonce,
|
|
1354
|
+
clientEphemeralPublicKeyJwk: input.keyPair.publicKeyJwk,
|
|
1355
|
+
serverEncryptionPublicKeyJwk: message.encryptionIdentity.publicKeyJwk,
|
|
1356
|
+
}), decodeBase64Url(message.encryptionChallengeNonce));
|
|
1357
|
+
if (proof.challengeNonce !== challengeNonce)
|
|
1358
|
+
return;
|
|
1359
|
+
if (proof.senderId !== message.senderId)
|
|
1360
|
+
return;
|
|
1361
|
+
if (proof.serverName !== input.address.serverName)
|
|
1362
|
+
return;
|
|
1363
|
+
ready.resolve({
|
|
1364
|
+
keyPair: input.keyPair,
|
|
1365
|
+
serverEncryptionPublicKeyJwk: message.encryptionIdentity.publicKeyJwk,
|
|
1366
|
+
serverEncryptionPublicKey,
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
catch {
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
input.mqtt.on('message', onMessage);
|
|
1374
|
+
const timer = setTimeout(() => {
|
|
1375
|
+
ready.reject(new Error(`Timed out bootstrapping encryption public key for ${input.address.serverName}`));
|
|
1376
|
+
}, timeoutMs);
|
|
1377
|
+
try {
|
|
1378
|
+
await publish(input.mqtt, resolvePlaintextTopic(input.options), {
|
|
1379
|
+
protocol: 'plat-css-v1',
|
|
1380
|
+
kind: 'discover',
|
|
1381
|
+
senderId: input.peerId,
|
|
1382
|
+
targetId: input.address.serverName,
|
|
1383
|
+
challengeNonce,
|
|
1384
|
+
requestEncryptionIdentity: true,
|
|
1385
|
+
bootstrapClientEphemeralPublicKeyJwk: input.keyPair.publicKeyJwk,
|
|
1386
|
+
at: Date.now(),
|
|
1387
|
+
});
|
|
1388
|
+
const secureState = await ready.promise;
|
|
1389
|
+
input.address.encryptionPublicKeyJwk = secureState.serverEncryptionPublicKeyJwk;
|
|
1390
|
+
return secureState;
|
|
1391
|
+
}
|
|
1392
|
+
finally {
|
|
1393
|
+
clearTimeout(timer);
|
|
1394
|
+
input.mqtt.off('message', onMessage);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
async function createPublicEncryptionBootstrapResponse(input) {
|
|
1398
|
+
if (!input.serverEncryptionKeyPair || !input.serverEncryptionIdentity || !input.challengeNonce) {
|
|
1399
|
+
return {};
|
|
1400
|
+
}
|
|
1401
|
+
const privateKey = await importX25519PrivateKeyJwk(input.serverEncryptionKeyPair.privateKeyJwk);
|
|
1402
|
+
const publicKey = await importX25519PublicKeyJwk(input.clientEphemeralPublicKeyJwk);
|
|
1403
|
+
const aeadKey = await deriveAeadKeyFromX25519(privateKey, publicKey, utf8('plat-css-sealed-signaling-v1'));
|
|
1404
|
+
const nonce = randomNonce12();
|
|
1405
|
+
const ciphertext = await encryptJsonAead(aeadKey, {
|
|
1406
|
+
challengeNonce: input.challengeNonce,
|
|
1407
|
+
senderId: input.serverInstanceId,
|
|
1408
|
+
serverName: input.serverName,
|
|
1409
|
+
}, buildBootstrapEncryptionAad({
|
|
1410
|
+
senderId: input.serverInstanceId,
|
|
1411
|
+
serverName: input.serverName,
|
|
1412
|
+
challengeNonce: input.challengeNonce,
|
|
1413
|
+
clientEphemeralPublicKeyJwk: input.clientEphemeralPublicKeyJwk,
|
|
1414
|
+
serverEncryptionPublicKeyJwk: input.serverEncryptionIdentity.publicKeyJwk,
|
|
1415
|
+
}), nonce);
|
|
1416
|
+
return {
|
|
1417
|
+
encryptionIdentity: input.serverEncryptionIdentity,
|
|
1418
|
+
encryptionChallengeCiphertext: encodeBase64Url(ciphertext),
|
|
1419
|
+
encryptionChallengeNonce: encodeBase64Url(nonce),
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
async function publishSealedClientPayload(input) {
|
|
1423
|
+
const nonce = randomNonce12();
|
|
1424
|
+
const envelope = await sealClientSideServerPayload({
|
|
1425
|
+
senderId: input.senderId,
|
|
1426
|
+
payload: input.payload,
|
|
1427
|
+
privateKey: input.secureState.keyPair.privateKey,
|
|
1428
|
+
peerPublicKey: input.secureState.serverEncryptionPublicKey,
|
|
1429
|
+
clientEphemeralPublicKeyJwk: input.secureState.keyPair.publicKeyJwk,
|
|
1430
|
+
serverEncryptionPublicKeyJwk: input.secureState.serverEncryptionPublicKeyJwk,
|
|
1431
|
+
maxSealedMessageBytes: input.options.maxSealedMessageBytes,
|
|
1432
|
+
nonce,
|
|
1433
|
+
});
|
|
1434
|
+
await publishRaw(input.mqtt, resolveSealedTopic(input.options), serializeSealedEnvelope(envelope.envelope));
|
|
1435
|
+
}
|
|
1436
|
+
async function sealClientSideServerPayloadForServer(input) {
|
|
1437
|
+
if (!input.serverEncryptionKeyPair) {
|
|
1438
|
+
throw new Error('Server encryption key pair is required for sealed signaling');
|
|
1439
|
+
}
|
|
1440
|
+
const privateKey = await importX25519PrivateKeyJwk(input.serverEncryptionKeyPair.privateKeyJwk);
|
|
1441
|
+
const publicKey = await importX25519PublicKeyJwk(input.clientEphemeralPublicKeyJwk);
|
|
1442
|
+
return {
|
|
1443
|
+
...(await sealClientSideServerPayload({
|
|
1444
|
+
senderId: input.senderId,
|
|
1445
|
+
payload: input.payload,
|
|
1446
|
+
privateKey,
|
|
1447
|
+
peerPublicKey: publicKey,
|
|
1448
|
+
clientEphemeralPublicKeyJwk: input.clientEphemeralPublicKeyJwk,
|
|
1449
|
+
serverEncryptionPublicKeyJwk: input.serverEncryptionKeyPair.publicKeyJwk,
|
|
1450
|
+
maxSealedMessageBytes: input.maxSealedMessageBytes,
|
|
1451
|
+
})),
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
async function sealClientSideServerPayload(input) {
|
|
1455
|
+
const nonceBytes = input.nonce ?? randomNonce12();
|
|
1456
|
+
const nonce = encodeBase64Url(nonceBytes);
|
|
1457
|
+
const at = Date.now();
|
|
1458
|
+
const sessionId = await computeSessionId({
|
|
1459
|
+
clientEphemeralPublicKeyJwk: input.clientEphemeralPublicKeyJwk,
|
|
1460
|
+
serverEncryptionPublicKeyJwk: input.serverEncryptionPublicKeyJwk,
|
|
1461
|
+
nonceB64u: nonce,
|
|
1462
|
+
}).catch(() => '');
|
|
1463
|
+
const aeadKey = await deriveAeadKeyFromX25519(input.privateKey, input.peerPublicKey, utf8('plat-css-sealed-signaling-v1'));
|
|
1464
|
+
const aadEnvelope = {
|
|
1465
|
+
platcss: 'sealed',
|
|
1466
|
+
version: 1,
|
|
1467
|
+
senderId: input.senderId,
|
|
1468
|
+
at,
|
|
1469
|
+
nonce,
|
|
1470
|
+
clientEphemeralPublicKeyJwk: input.clientEphemeralPublicKeyJwk,
|
|
1471
|
+
};
|
|
1472
|
+
const ciphertext = await encryptJsonAead(aeadKey, input.payload, utf8(stableJson(aadEnvelope)), nonceBytes);
|
|
1473
|
+
const bucket = choosePaddingBucket(ciphertext.byteLength);
|
|
1474
|
+
const padded = padCiphertext(ciphertext, bucket);
|
|
1475
|
+
const maxBytes = input.maxSealedMessageBytes ?? DEFAULT_CLIENT_SIDE_SERVER_MAX_SEALED_MESSAGE_BYTES;
|
|
1476
|
+
if (padded.byteLength > maxBytes) {
|
|
1477
|
+
throw new Error(`Sealed signaling message exceeds maximum size of ${maxBytes} bytes`);
|
|
1478
|
+
}
|
|
1479
|
+
return {
|
|
1480
|
+
sessionId,
|
|
1481
|
+
envelope: {
|
|
1482
|
+
...aadEnvelope,
|
|
1483
|
+
ciphertext: encodeBase64Url(padded),
|
|
1484
|
+
},
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
function parseSealedEnvelope(payload) {
|
|
1488
|
+
try {
|
|
1489
|
+
const text = payload instanceof Uint8Array ? new TextDecoder().decode(payload) : String(payload);
|
|
1490
|
+
const parsed = JSON.parse(text);
|
|
1491
|
+
if (!isClientSideServerSealedEnvelope(parsed)) {
|
|
1492
|
+
debugSecure('sealed envelope parse failed', { reason: 'validator' });
|
|
1493
|
+
return null;
|
|
1494
|
+
}
|
|
1495
|
+
return parsed;
|
|
1496
|
+
}
|
|
1497
|
+
catch {
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
function buildSealedEnvelopeAad(envelope) {
|
|
1502
|
+
return utf8(stableJson({
|
|
1503
|
+
platcss: 'sealed',
|
|
1504
|
+
version: 1,
|
|
1505
|
+
senderId: envelope.senderId,
|
|
1506
|
+
at: envelope.at,
|
|
1507
|
+
nonce: envelope.nonce,
|
|
1508
|
+
clientEphemeralPublicKeyJwk: envelope.clientEphemeralPublicKeyJwk,
|
|
1509
|
+
}));
|
|
1510
|
+
}
|
|
1511
|
+
function buildBootstrapEncryptionAad(input) {
|
|
1512
|
+
return utf8(stableJson({
|
|
1513
|
+
protocol: 'plat-css-v1',
|
|
1514
|
+
kind: 'announce',
|
|
1515
|
+
senderId: input.senderId,
|
|
1516
|
+
serverName: input.serverName,
|
|
1517
|
+
challengeNonce: input.challengeNonce,
|
|
1518
|
+
bootstrapClientEphemeralPublicKeyJwk: input.clientEphemeralPublicKeyJwk,
|
|
1519
|
+
encryptionPublicKeyJwk: input.serverEncryptionPublicKeyJwk,
|
|
1520
|
+
}));
|
|
1521
|
+
}
|
|
1522
|
+
function serializeSealedEnvelope(envelope) {
|
|
1523
|
+
return JSON.stringify({
|
|
1524
|
+
platcss: envelope.platcss,
|
|
1525
|
+
version: envelope.version,
|
|
1526
|
+
senderId: envelope.senderId,
|
|
1527
|
+
at: envelope.at,
|
|
1528
|
+
nonce: envelope.nonce,
|
|
1529
|
+
clientEphemeralPublicKeyJwk: envelope.clientEphemeralPublicKeyJwk,
|
|
1530
|
+
ciphertext: envelope.ciphertext,
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
function resolveAddressAuth(address) {
|
|
1534
|
+
const username = address.params.username;
|
|
1535
|
+
const password = address.params.password;
|
|
1536
|
+
if (!username || !password)
|
|
1537
|
+
return undefined;
|
|
1538
|
+
return { username, password };
|
|
1539
|
+
}
|
|
1540
|
+
function getAuthoritySigningPublicKey(record) {
|
|
1541
|
+
return isClientSideServerSignedAuthorityRecordV2(record)
|
|
1542
|
+
? record.signingPublicKeyJwk
|
|
1543
|
+
: record.publicKeyJwk;
|
|
1544
|
+
}
|
|
1545
|
+
function normalizeExpectedIdentity(record) {
|
|
1546
|
+
if (isClientSideServerTrustedServerRecordV2(record)) {
|
|
1547
|
+
return {
|
|
1548
|
+
trustedRecord: record,
|
|
1549
|
+
signing: {
|
|
1550
|
+
algorithm: 'ECDSA-P256',
|
|
1551
|
+
publicKeyJwk: record.signingPublicKeyJwk,
|
|
1552
|
+
keyId: record.signingKeyId ?? 'signing-key',
|
|
1553
|
+
fingerprint: record.signingFingerprint,
|
|
1554
|
+
},
|
|
1555
|
+
encryption: {
|
|
1556
|
+
algorithm: 'X25519',
|
|
1557
|
+
publicKeyJwk: record.encryptionPublicKeyJwk,
|
|
1558
|
+
keyId: record.encryptionKeyId ?? 'encryption-key',
|
|
1559
|
+
fingerprint: record.encryptionFingerprint,
|
|
1560
|
+
},
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
return {
|
|
1564
|
+
trustedRecord: record,
|
|
1565
|
+
signing: {
|
|
1566
|
+
algorithm: 'ECDSA-P256',
|
|
1567
|
+
publicKeyJwk: record.publicKeyJwk,
|
|
1568
|
+
keyId: record.keyId ?? 'signing-key',
|
|
1569
|
+
fingerprint: record.fingerprint,
|
|
1570
|
+
},
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
function debugSecure(event, details) {
|
|
1574
|
+
if (!globalThis.__PLAT_CSS_DEBUG_SECURE)
|
|
1575
|
+
return;
|
|
1576
|
+
console.debug(`[plat-css secure] ${event}`, details ?? {});
|
|
1577
|
+
}
|
|
793
1578
|
function deferred() {
|
|
794
1579
|
let resolve;
|
|
795
1580
|
let reject;
|