@modularizer/plat 0.8.0 → 0.10.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/README.md +75 -0
- package/dist/client-entry.d.ts +1 -0
- package/dist/client-entry.d.ts.map +1 -1
- package/dist/client-entry.js +1 -0
- package/dist/client-entry.js.map +1 -1
- package/dist/client-server-entry.d.ts +5 -0
- package/dist/client-server-entry.d.ts.map +1 -1
- package/dist/client-server-entry.js +5 -0
- package/dist/client-server-entry.js.map +1 -1
- package/dist/client-side-python/runtime.d.ts +1 -0
- package/dist/client-side-python/runtime.d.ts.map +1 -1
- package/dist/client-side-python/runtime.js +18 -1
- package/dist/client-side-python/runtime.js.map +1 -1
- package/dist/client-side-server/channel.d.ts +1 -1
- package/dist/client-side-server/channel.d.ts.map +1 -1
- package/dist/client-side-server/channel.js +113 -4
- 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 +38 -6
- package/dist/client-side-server/mqtt-webrtc.d.ts.map +1 -1
- package/dist/client-side-server/mqtt-webrtc.js +843 -68
- package/dist/client-side-server/mqtt-webrtc.js.map +1 -1
- package/dist/client-side-server/plat-bridge.d.ts +40 -0
- package/dist/client-side-server/plat-bridge.d.ts.map +1 -0
- package/dist/client-side-server/plat-bridge.js +876 -0
- package/dist/client-side-server/plat-bridge.js.map +1 -0
- package/dist/client-side-server/plat-fetch.d.ts +33 -0
- package/dist/client-side-server/plat-fetch.d.ts.map +1 -0
- package/dist/client-side-server/plat-fetch.js +199 -0
- package/dist/client-side-server/plat-fetch.js.map +1 -0
- package/dist/client-side-server/plat-service-worker-client.d.ts +30 -0
- package/dist/client-side-server/plat-service-worker-client.d.ts.map +1 -0
- package/dist/client-side-server/plat-service-worker-client.js +141 -0
- package/dist/client-side-server/plat-service-worker-client.js.map +1 -0
- package/dist/client-side-server/plat-service-worker.d.ts +35 -0
- package/dist/client-side-server/plat-service-worker.d.ts.map +1 -0
- package/dist/client-side-server/plat-service-worker.js +292 -0
- package/dist/client-side-server/plat-service-worker.js.map +1 -0
- package/dist/client-side-server/protocol.d.ts +90 -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 +9 -0
- package/dist/client-side-server/runtime.d.ts.map +1 -1
- package/dist/client-side-server/runtime.js +44 -13
- 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 +17 -0
- package/dist/client-side-server/server.d.ts.map +1 -1
- package/dist/client-side-server/server.js +111 -7
- package/dist/client-side-server/server.js.map +1 -1
- package/dist/client-side-server/signaling.d.ts +1 -0
- package/dist/client-side-server/signaling.d.ts.map +1 -1
- package/dist/client-side-server/signaling.js.map +1 -1
- package/dist/generated/python-browser-sources.js +4 -4
- package/dist/generated/python-browser-sources.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/core.d.ts +8 -0
- package/dist/server/core.d.ts.map +1 -1
- package/dist/server/core.js +21 -0
- package/dist/server/core.js.map +1 -1
- package/dist/server/server.d.ts +4 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +81 -2
- package/dist/server/server.js.map +1 -1
- package/dist/static/file-response.d.ts +42 -0
- package/dist/static/file-response.d.ts.map +1 -0
- package/dist/static/file-response.js +57 -0
- package/dist/static/file-response.js.map +1 -0
- package/dist/static/glob-match.d.ts +16 -0
- package/dist/static/glob-match.d.ts.map +1 -0
- package/dist/static/glob-match.js +48 -0
- package/dist/static/glob-match.js.map +1 -0
- package/dist/static/index.d.ts +9 -0
- package/dist/static/index.d.ts.map +1 -0
- package/dist/static/index.js +6 -0
- package/dist/static/index.js.map +1 -0
- package/dist/static/mime-types.d.ts +6 -0
- package/dist/static/mime-types.d.ts.map +1 -0
- package/dist/static/mime-types.js +74 -0
- package/dist/static/mime-types.js.map +1 -0
- package/dist/static/static-folder.d.ts +69 -0
- package/dist/static/static-folder.d.ts.map +1 -0
- package/dist/static/static-folder.js +206 -0
- package/dist/static/static-folder.js.map +1 -0
- package/dist/static/virtual-file-system.d.ts +45 -0
- package/dist/static/virtual-file-system.d.ts.map +1 -0
- package/dist/static/virtual-file-system.js +100 -0
- package/dist/static/virtual-file-system.js.map +1 -0
- package/package.json +5 -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
|
+
}
|
|
90
136
|
return;
|
|
91
|
-
|
|
137
|
+
}
|
|
138
|
+
if (!isSecureSignalingEnabled(this.options))
|
|
92
139
|
return;
|
|
93
|
-
|
|
94
|
-
|
|
140
|
+
const opened = await openClientSideServerSealedEnvelopeForServer({
|
|
141
|
+
envelopePayload: payload,
|
|
142
|
+
serverEncryptionKeyPair: this.encryptionKeyPair,
|
|
143
|
+
});
|
|
144
|
+
if (!opened || envelopeIsReplayedOrStale(opened.payload, this.options))
|
|
145
|
+
return;
|
|
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(),
|
|
@@ -195,7 +270,7 @@ export class ClientSideServerMQTTWebRTCServer {
|
|
|
195
270
|
this.options.workerInfo.acceptingNewClients = false;
|
|
196
271
|
}
|
|
197
272
|
if (this.mqtt) {
|
|
198
|
-
await
|
|
273
|
+
await publishJson(this.mqtt, resolvePlaintextTopic(this.options), {
|
|
199
274
|
protocol: 'plat-css-v1',
|
|
200
275
|
kind: 'announce',
|
|
201
276
|
senderId: this.serverInstanceId,
|
|
@@ -221,7 +296,7 @@ export class ClientSideServerMQTTWebRTCServer {
|
|
|
221
296
|
peer.onicecandidate = (event) => {
|
|
222
297
|
if (!event.candidate || !this.mqtt)
|
|
223
298
|
return;
|
|
224
|
-
void
|
|
299
|
+
void publishJson(this.mqtt, resolvePlaintextTopic(this.options), {
|
|
225
300
|
protocol: 'plat-css-v1',
|
|
226
301
|
kind: 'ice',
|
|
227
302
|
senderId: this.serverInstanceId,
|
|
@@ -265,7 +340,7 @@ export class ClientSideServerMQTTWebRTCServer {
|
|
|
265
340
|
this.pendingCandidates.delete(message.connectionId);
|
|
266
341
|
const answer = await peer.createAnswer();
|
|
267
342
|
await peer.setLocalDescription(answer);
|
|
268
|
-
await
|
|
343
|
+
await publishJson(this.mqtt, resolvePlaintextTopic(this.options), {
|
|
269
344
|
protocol: 'plat-css-v1',
|
|
270
345
|
kind: 'answer',
|
|
271
346
|
senderId: this.serverInstanceId,
|
|
@@ -286,6 +361,113 @@ export class ClientSideServerMQTTWebRTCServer {
|
|
|
286
361
|
at: Date.now(),
|
|
287
362
|
});
|
|
288
363
|
}
|
|
364
|
+
async acceptSealedOffer(payload, response) {
|
|
365
|
+
if (!this.mqtt)
|
|
366
|
+
return;
|
|
367
|
+
await this.ensureIdentity();
|
|
368
|
+
await this.ensureEncryptionIdentity();
|
|
369
|
+
const authOk = await this.authorizeSealedOffer(payload, response);
|
|
370
|
+
if (!authOk)
|
|
371
|
+
return;
|
|
372
|
+
if (this.options.workerInfo?.acceptingNewClients === false) {
|
|
373
|
+
await this.publishSealedResponse(response, {
|
|
374
|
+
type: 'reject',
|
|
375
|
+
connectionId: payload.connectionId,
|
|
376
|
+
serverName: this.options.serverName,
|
|
377
|
+
reason: 'server-not-accepting',
|
|
378
|
+
at: Date.now(),
|
|
379
|
+
});
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const peer = new RTCPeerConnection({
|
|
383
|
+
iceServers: this.options.iceServers ?? DEFAULT_CLIENT_SIDE_SERVER_ICE_SERVERS,
|
|
384
|
+
});
|
|
385
|
+
this.peers.set(payload.connectionId, peer);
|
|
386
|
+
peer.onicecandidate = (event) => {
|
|
387
|
+
if (!event.candidate || !this.mqtt)
|
|
388
|
+
return;
|
|
389
|
+
void this.publishSealedResponse(response, {
|
|
390
|
+
type: 'ice',
|
|
391
|
+
connectionId: payload.connectionId,
|
|
392
|
+
serverName: this.options.serverName,
|
|
393
|
+
candidate: event.candidate.toJSON(),
|
|
394
|
+
at: Date.now(),
|
|
395
|
+
});
|
|
396
|
+
};
|
|
397
|
+
peer.onconnectionstatechange = () => {
|
|
398
|
+
if (peer.connectionState === 'failed' || peer.connectionState === 'closed' || peer.connectionState === 'disconnected') {
|
|
399
|
+
this.unsubscribeByConnection.get(payload.connectionId)?.();
|
|
400
|
+
this.unsubscribeByConnection.delete(payload.connectionId);
|
|
401
|
+
this.peers.delete(payload.connectionId);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
peer.ondatachannel = (event) => {
|
|
405
|
+
const channel = createRTCDataChannelAdapter(event.channel);
|
|
406
|
+
const connId = payload.connectionId;
|
|
407
|
+
const controlUnsubscribe = channel.subscribe(async (msg) => {
|
|
408
|
+
await this.handleControlMessage(msg, channel, connId);
|
|
409
|
+
});
|
|
410
|
+
const serveUnsubscribe = this.options.server.serveChannel(channel);
|
|
411
|
+
this.channels.set(connId, channel);
|
|
412
|
+
this.clientCount++;
|
|
413
|
+
const cleanup = () => {
|
|
414
|
+
controlUnsubscribe();
|
|
415
|
+
serveUnsubscribe();
|
|
416
|
+
this.channels.delete(connId);
|
|
417
|
+
this.clientCount--;
|
|
418
|
+
this.unsubscribeByConnection.delete(connId);
|
|
419
|
+
};
|
|
420
|
+
this.unsubscribeByConnection.set(connId, cleanup);
|
|
421
|
+
event.channel.addEventListener('close', cleanup, { once: true });
|
|
422
|
+
};
|
|
423
|
+
try {
|
|
424
|
+
await peer.setRemoteDescription(new RTCSessionDescription(payload.description));
|
|
425
|
+
for (const candidate of this.pendingCandidates.get(payload.connectionId) ?? []) {
|
|
426
|
+
await peer.addIceCandidate(new RTCIceCandidate(candidate));
|
|
427
|
+
}
|
|
428
|
+
this.pendingCandidates.delete(payload.connectionId);
|
|
429
|
+
const answer = await peer.createAnswer();
|
|
430
|
+
await peer.setLocalDescription(answer);
|
|
431
|
+
await this.publishSealedResponse(response, {
|
|
432
|
+
type: 'answer',
|
|
433
|
+
connectionId: payload.connectionId,
|
|
434
|
+
serverName: this.options.serverName,
|
|
435
|
+
description: answer,
|
|
436
|
+
identity: this.publicIdentity,
|
|
437
|
+
authorityRecord: this.options.identity?.authorityRecord,
|
|
438
|
+
challengeNonce: payload.challengeNonce,
|
|
439
|
+
challengeSignature: payload.challengeNonce
|
|
440
|
+
? await signClientSideServerChallenge(this.identityKeyPair, buildClientSideServerIdentityChallenge({
|
|
441
|
+
serverName: this.options.serverName,
|
|
442
|
+
connectionId: payload.connectionId,
|
|
443
|
+
challengeNonce: payload.challengeNonce,
|
|
444
|
+
}))
|
|
445
|
+
: undefined,
|
|
446
|
+
at: Date.now(),
|
|
447
|
+
});
|
|
448
|
+
debugSecure('secure answer sent', {
|
|
449
|
+
serverName: this.options.serverName,
|
|
450
|
+
connectionId: payload.connectionId,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
debugSecure('sealed offer processing failed', {
|
|
455
|
+
serverName: this.options.serverName,
|
|
456
|
+
connectionId: payload.connectionId,
|
|
457
|
+
error: error instanceof Error ? error.message : String(error),
|
|
458
|
+
});
|
|
459
|
+
await this.publishSealedResponse(response, {
|
|
460
|
+
type: 'reject',
|
|
461
|
+
connectionId: payload.connectionId,
|
|
462
|
+
serverName: this.options.serverName,
|
|
463
|
+
reason: 'bad-message',
|
|
464
|
+
at: Date.now(),
|
|
465
|
+
});
|
|
466
|
+
peer.close();
|
|
467
|
+
this.peers.delete(payload.connectionId);
|
|
468
|
+
this.pendingCandidates.delete(payload.connectionId);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
289
471
|
async ensureIdentity() {
|
|
290
472
|
if (this.identityKeyPair && this.publicIdentity)
|
|
291
473
|
return;
|
|
@@ -297,6 +479,105 @@ export class ClientSideServerMQTTWebRTCServer {
|
|
|
297
479
|
});
|
|
298
480
|
this.publicIdentity = await toClientSideServerPublicIdentity(this.identityKeyPair);
|
|
299
481
|
}
|
|
482
|
+
async ensureEncryptionIdentity() {
|
|
483
|
+
if (this.encryptionKeyPair && this.encryptionPublicIdentity)
|
|
484
|
+
return;
|
|
485
|
+
if (this.options.serverEncryptionKeyPair) {
|
|
486
|
+
this.encryptionKeyPair = this.options.serverEncryptionKeyPair;
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
this.encryptionKeyPair = await generateClientSideServerEncryptionKeyPair();
|
|
490
|
+
}
|
|
491
|
+
this.encryptionPublicIdentity = await toClientSideServerEncryptionPublicIdentity(this.encryptionKeyPair);
|
|
492
|
+
}
|
|
493
|
+
isSealedReplay(senderId, nonce) {
|
|
494
|
+
this.pruneSeenSealedNonces();
|
|
495
|
+
return this.seenSealedNonces.has(`${senderId}:${nonce}`);
|
|
496
|
+
}
|
|
497
|
+
rememberSealedNonce(senderId, nonce) {
|
|
498
|
+
this.pruneSeenSealedNonces();
|
|
499
|
+
this.seenSealedNonces.set(`${senderId}:${nonce}`, Date.now());
|
|
500
|
+
}
|
|
501
|
+
pruneSeenSealedNonces() {
|
|
502
|
+
const cutoff = Date.now() - (this.options.replayWindowMs ?? DEFAULT_CLIENT_SIDE_SERVER_REPLAY_WINDOW_MS);
|
|
503
|
+
for (const [key, value] of this.seenSealedNonces.entries()) {
|
|
504
|
+
if (value < cutoff) {
|
|
505
|
+
this.seenSealedNonces.delete(key);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async publishSealedResponse(response, payload) {
|
|
510
|
+
if (!this.mqtt)
|
|
511
|
+
return;
|
|
512
|
+
const sealed = await sealClientSideServerPayloadForServer({
|
|
513
|
+
senderId: this.serverInstanceId,
|
|
514
|
+
payload,
|
|
515
|
+
serverEncryptionKeyPair: this.encryptionKeyPair,
|
|
516
|
+
clientEphemeralPublicKeyJwk: response.clientEphemeralPublicKeyJwk,
|
|
517
|
+
maxSealedMessageBytes: this.options.maxSealedMessageBytes,
|
|
518
|
+
});
|
|
519
|
+
if (payload.type === 'reject') {
|
|
520
|
+
debugSecure('sealed reject sent', {
|
|
521
|
+
serverName: this.options.serverName,
|
|
522
|
+
connectionId: payload.connectionId,
|
|
523
|
+
reason: payload.reason,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
await publishRaw(this.mqtt, resolveSealedTopic(this.options), serializeSealedEnvelope(sealed.envelope));
|
|
527
|
+
}
|
|
528
|
+
async authorizeSealedOffer(payload, response) {
|
|
529
|
+
const signalingAuth = this.options.signalingAuth;
|
|
530
|
+
if (!signalingAuth)
|
|
531
|
+
return true;
|
|
532
|
+
if (!payload.auth) {
|
|
533
|
+
if (signalingAuth.required !== false) {
|
|
534
|
+
await this.publishSealedResponse(response, {
|
|
535
|
+
type: 'reject',
|
|
536
|
+
connectionId: payload.connectionId,
|
|
537
|
+
serverName: this.options.serverName,
|
|
538
|
+
reason: 'auth-required',
|
|
539
|
+
at: Date.now(),
|
|
540
|
+
});
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
if (typeof signalingAuth.verify !== 'function') {
|
|
546
|
+
if (signalingAuth.required !== false) {
|
|
547
|
+
await this.publishSealedResponse(response, {
|
|
548
|
+
type: 'reject',
|
|
549
|
+
connectionId: payload.connectionId,
|
|
550
|
+
serverName: this.options.serverName,
|
|
551
|
+
reason: 'auth-required',
|
|
552
|
+
at: Date.now(),
|
|
553
|
+
});
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
let verified = false;
|
|
559
|
+
try {
|
|
560
|
+
verified = await signalingAuth.verify(payload.auth, {
|
|
561
|
+
serverName: this.options.serverName,
|
|
562
|
+
connectionId: payload.connectionId,
|
|
563
|
+
senderId: response.senderId,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
verified = false;
|
|
568
|
+
}
|
|
569
|
+
if (!verified) {
|
|
570
|
+
await this.publishSealedResponse(response, {
|
|
571
|
+
type: 'reject',
|
|
572
|
+
connectionId: payload.connectionId,
|
|
573
|
+
serverName: this.options.serverName,
|
|
574
|
+
reason: 'auth-failed',
|
|
575
|
+
at: Date.now(),
|
|
576
|
+
});
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
300
581
|
}
|
|
301
582
|
export function createClientSideServerMQTTWebRTCTransportPlugin(options = {}) {
|
|
302
583
|
if (isDefaultMQTTWebRTCOptions(options)) {
|
|
@@ -390,7 +671,7 @@ function createClientSideServerMQTTWebRTCPeerPoolInternal(options) {
|
|
|
390
671
|
}
|
|
391
672
|
export async function discoverClientSideServers(serverName, options = {}) {
|
|
392
673
|
const mqttClient = await connectToBroker(options);
|
|
393
|
-
const topic = options
|
|
674
|
+
const topic = resolvePlaintextTopic(options);
|
|
394
675
|
const peerId = `${options.clientIdPrefix ?? 'client'}:${randomId('discover')}`;
|
|
395
676
|
const challengeNonce = randomId('challenge') + '-mqtt';
|
|
396
677
|
const timeoutMs = options.workerPool?.discoveryTimeoutMs ?? 3000;
|
|
@@ -416,7 +697,7 @@ export async function discoverClientSideServers(serverName, options = {}) {
|
|
|
416
697
|
mqttChallengeVerified = await verifyClientSideServerChallenge(message.identity.publicKeyJwk, challengeString, message.challengeSignature);
|
|
417
698
|
}
|
|
418
699
|
if (message.authorityRecord && options.identity?.authority?.publicKeyJwk) {
|
|
419
|
-
const authorityOk = await
|
|
700
|
+
const authorityOk = await verifyAnySignedClientSideServerAuthorityRecord(message.authorityRecord, options.identity.authority.publicKeyJwk);
|
|
420
701
|
if (!authorityOk)
|
|
421
702
|
return;
|
|
422
703
|
}
|
|
@@ -502,7 +783,8 @@ export async function connectFirstDiscovery(address, options) {
|
|
|
502
783
|
export async function createClientSideServerMQTTWebRTCPeerSession(address, options, targetInstanceId) {
|
|
503
784
|
const webrtc = await resolveClientWebRTCImplementation();
|
|
504
785
|
const mqtt = await connectToBroker(options);
|
|
505
|
-
const
|
|
786
|
+
const plaintextTopic = resolvePlaintextTopic(options);
|
|
787
|
+
const sealedTopic = resolveSealedTopic(options);
|
|
506
788
|
const peerId = `${options.clientIdPrefix ?? 'client'}:${randomId('peer')}`;
|
|
507
789
|
const connectionId = randomId('conn');
|
|
508
790
|
const challengeNonce = randomId('challenge');
|
|
@@ -515,15 +797,77 @@ export async function createClientSideServerMQTTWebRTCPeerSession(address, optio
|
|
|
515
797
|
const cleanupCallbacks = [];
|
|
516
798
|
const pendingCandidates = [];
|
|
517
799
|
const expectedIdentity = await resolveExpectedServerIdentity(address.serverName, options.identity);
|
|
800
|
+
let secureState;
|
|
518
801
|
let open = true;
|
|
802
|
+
let answerApplied = false;
|
|
519
803
|
let serverIdentity;
|
|
520
|
-
await subscribe(mqtt,
|
|
804
|
+
await subscribe(mqtt, plaintextTopic);
|
|
805
|
+
if (sealedTopic !== plaintextTopic) {
|
|
806
|
+
await subscribe(mqtt, sealedTopic);
|
|
807
|
+
}
|
|
808
|
+
if (isSecureSignalingEnabled(options)) {
|
|
809
|
+
secureState = await createSecureClientSessionState({
|
|
810
|
+
mqtt,
|
|
811
|
+
address,
|
|
812
|
+
options,
|
|
813
|
+
peerId,
|
|
814
|
+
expectedIdentity,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
521
817
|
const onMessage = async (_topic, payload) => {
|
|
818
|
+
if (secureState) {
|
|
819
|
+
const opened = await openClientSideServerSealedEnvelopeForClient({
|
|
820
|
+
envelopePayload: payload,
|
|
821
|
+
secureState,
|
|
822
|
+
});
|
|
823
|
+
if (!opened || opened.envelope.senderId === peerId || opened.payload.connectionId !== connectionId)
|
|
824
|
+
return;
|
|
825
|
+
if (opened.payload.type === 'reject') {
|
|
826
|
+
ready.reject(new Error(`Client-side server ${address.serverName} rejected connection: ${opened.payload.reason}`));
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (opened.payload.type === 'answer' && opened.payload.description) {
|
|
830
|
+
if (answerApplied)
|
|
831
|
+
return;
|
|
832
|
+
answerApplied = true;
|
|
833
|
+
await verifyServerIdentityForSealedAnswer({
|
|
834
|
+
serverName: address.serverName,
|
|
835
|
+
connectionId,
|
|
836
|
+
challengeNonce,
|
|
837
|
+
message: opened.payload,
|
|
838
|
+
optionsIdentity: options.identity,
|
|
839
|
+
expectedIdentity,
|
|
840
|
+
});
|
|
841
|
+
serverIdentity = opened.payload.identity;
|
|
842
|
+
await peer.setRemoteDescription(webrtc.createSessionDescription(opened.payload.description));
|
|
843
|
+
await maybeTrustServerAfterAnswer(address.serverName, expectedIdentity, opened.payload, options.identity);
|
|
844
|
+
for (const candidate of pendingCandidates.splice(0, pendingCandidates.length)) {
|
|
845
|
+
await peer.addIceCandidate(webrtc.createIceCandidate(candidate));
|
|
846
|
+
}
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (opened.payload.type === 'ice') {
|
|
850
|
+
debugSecure('secure ICE received', {
|
|
851
|
+
serverName: address.serverName,
|
|
852
|
+
connectionId,
|
|
853
|
+
});
|
|
854
|
+
if (peer.remoteDescription) {
|
|
855
|
+
await peer.addIceCandidate(webrtc.createIceCandidate(opened.payload.candidate));
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
pendingCandidates.push(opened.payload.candidate);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
522
863
|
const message = parseSignalingMessage(payload);
|
|
523
864
|
if (!message || message.senderId === peerId || message.targetId !== peerId || message.connectionId !== connectionId) {
|
|
524
865
|
return;
|
|
525
866
|
}
|
|
526
867
|
if (message.kind === 'answer' && message.description) {
|
|
868
|
+
if (answerApplied)
|
|
869
|
+
return;
|
|
870
|
+
answerApplied = true;
|
|
527
871
|
await verifyServerIdentityForAnswer({
|
|
528
872
|
serverName: address.serverName,
|
|
529
873
|
connectionId,
|
|
@@ -534,24 +878,7 @@ export async function createClientSideServerMQTTWebRTCPeerSession(address, optio
|
|
|
534
878
|
});
|
|
535
879
|
serverIdentity = message.identity;
|
|
536
880
|
await peer.setRemoteDescription(webrtc.createSessionDescription(message.description));
|
|
537
|
-
|
|
538
|
-
const trusted = await trustClientSideServerFromAuthorityRecord(message.authorityRecord, options.identity.authority.publicKeyJwk, {
|
|
539
|
-
storage: options.identity?.knownHostsStorage,
|
|
540
|
-
storageKey: options.identity?.knownHostsStorageKey,
|
|
541
|
-
});
|
|
542
|
-
if (options.identity?.knownHosts) {
|
|
543
|
-
saveTrustedClientSideServerRecordToMap(options.identity.knownHosts, trusted);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
else if (!expectedIdentity && options.identity?.trustOnFirstUse !== false && message.identity) {
|
|
547
|
-
const trusted = await trustClientSideServerOnFirstUse(address.serverName, message.identity, {
|
|
548
|
-
storage: options.identity?.knownHostsStorage,
|
|
549
|
-
storageKey: options.identity?.knownHostsStorageKey,
|
|
550
|
-
});
|
|
551
|
-
if (options.identity?.knownHosts) {
|
|
552
|
-
saveTrustedClientSideServerRecordToMap(options.identity.knownHosts, trusted);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
881
|
+
await maybeTrustServerAfterAnswer(address.serverName, expectedIdentity, message, options.identity);
|
|
555
882
|
for (const candidate of pendingCandidates.splice(0, pendingCandidates.length)) {
|
|
556
883
|
await peer.addIceCandidate(webrtc.createIceCandidate(candidate));
|
|
557
884
|
}
|
|
@@ -569,10 +896,29 @@ export async function createClientSideServerMQTTWebRTCPeerSession(address, optio
|
|
|
569
896
|
mqtt.on('message', onMessage);
|
|
570
897
|
cleanupCallbacks.push(() => mqtt.off('message', onMessage));
|
|
571
898
|
const offerTarget = targetInstanceId ?? address.serverName;
|
|
899
|
+
const addressAuth = resolveAddressAuth(address);
|
|
572
900
|
peer.onicecandidate = (event) => {
|
|
573
901
|
if (!event.candidate)
|
|
574
902
|
return;
|
|
575
|
-
|
|
903
|
+
if (secureState) {
|
|
904
|
+
void publishSealedClientPayload({
|
|
905
|
+
mqtt,
|
|
906
|
+
options,
|
|
907
|
+
senderId: peerId,
|
|
908
|
+
secureState,
|
|
909
|
+
payload: {
|
|
910
|
+
type: 'ice',
|
|
911
|
+
connectionId,
|
|
912
|
+
serverName: address.serverName,
|
|
913
|
+
candidate: webrtc.serializeIceCandidate(event.candidate),
|
|
914
|
+
at: Date.now(),
|
|
915
|
+
},
|
|
916
|
+
}).then(() => {
|
|
917
|
+
debugSecure('secure ICE sent', { serverName: address.serverName, connectionId });
|
|
918
|
+
});
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
void publish(mqtt, plaintextTopic, {
|
|
576
922
|
protocol: 'plat-css-v1',
|
|
577
923
|
kind: 'ice',
|
|
578
924
|
senderId: peerId,
|
|
@@ -598,22 +944,52 @@ export async function createClientSideServerMQTTWebRTCPeerSession(address, optio
|
|
|
598
944
|
}, { once: true });
|
|
599
945
|
const offer = await peer.createOffer();
|
|
600
946
|
await peer.setLocalDescription(offer);
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
947
|
+
if (secureState) {
|
|
948
|
+
await publishSealedClientPayload({
|
|
949
|
+
mqtt,
|
|
950
|
+
options,
|
|
951
|
+
senderId: peerId,
|
|
952
|
+
secureState,
|
|
953
|
+
payload: {
|
|
954
|
+
type: 'offer',
|
|
955
|
+
connectionId,
|
|
956
|
+
serverName: address.serverName,
|
|
957
|
+
description: offer,
|
|
958
|
+
challengeNonce,
|
|
959
|
+
requirePrivateChallenge: options.requirePrivateChallenge,
|
|
960
|
+
auth: addressAuth,
|
|
961
|
+
at: Date.now(),
|
|
962
|
+
},
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
await publish(mqtt, plaintextTopic, {
|
|
967
|
+
protocol: 'plat-css-v1',
|
|
968
|
+
kind: 'offer',
|
|
969
|
+
senderId: peerId,
|
|
970
|
+
targetId: offerTarget,
|
|
971
|
+
serverName: address.serverName,
|
|
972
|
+
connectionId,
|
|
973
|
+
description: offer,
|
|
974
|
+
challengeNonce,
|
|
975
|
+
at: Date.now(),
|
|
976
|
+
});
|
|
977
|
+
}
|
|
612
978
|
const timer = setTimeout(() => {
|
|
613
979
|
ready.reject(new Error(`Timed out connecting to client-side server ${address.serverName}`));
|
|
614
980
|
}, timeoutMs);
|
|
615
981
|
cleanupCallbacks.push(() => clearTimeout(timer));
|
|
616
|
-
|
|
982
|
+
try {
|
|
983
|
+
await ready.promise;
|
|
984
|
+
}
|
|
985
|
+
catch (error) {
|
|
986
|
+
open = false;
|
|
987
|
+
for (const cleanup of cleanupCallbacks)
|
|
988
|
+
cleanup();
|
|
989
|
+
peer.close();
|
|
990
|
+
await endClient(mqtt);
|
|
991
|
+
throw error;
|
|
992
|
+
}
|
|
617
993
|
const channel = createRTCDataChannelAdapter(dataChannel);
|
|
618
994
|
const originalClose = channel.close?.bind(channel);
|
|
619
995
|
return {
|
|
@@ -655,7 +1031,7 @@ export async function createClientSideServerMQTTWebRTCPeerSession(address, optio
|
|
|
655
1031
|
async function resolveExpectedServerIdentity(serverName, options) {
|
|
656
1032
|
const mapped = loadTrustedClientSideServerRecordFromMap(options?.knownHosts, serverName);
|
|
657
1033
|
if (mapped)
|
|
658
|
-
return mapped;
|
|
1034
|
+
return normalizeExpectedIdentity(mapped);
|
|
659
1035
|
if (options?.authorityServers?.length) {
|
|
660
1036
|
const trusted = await resolveTrustedClientSideServerFromAuthorities(serverName, options.authorityServers, {
|
|
661
1037
|
storage: options?.knownHostsStorage,
|
|
@@ -665,12 +1041,12 @@ async function resolveExpectedServerIdentity(serverName, options) {
|
|
|
665
1041
|
if (options.knownHosts) {
|
|
666
1042
|
saveTrustedClientSideServerRecordToMap(options.knownHosts, trusted);
|
|
667
1043
|
}
|
|
668
|
-
return trusted;
|
|
1044
|
+
return normalizeExpectedIdentity(trusted);
|
|
669
1045
|
}
|
|
670
1046
|
}
|
|
671
1047
|
const authorityResolved = options?.authorityResolver ? await options.authorityResolver(serverName) : null;
|
|
672
1048
|
if (authorityResolved)
|
|
673
|
-
return authorityResolved;
|
|
1049
|
+
return normalizeExpectedIdentity(authorityResolved);
|
|
674
1050
|
const stored = loadTrustedClientSideServerRecord(serverName, {
|
|
675
1051
|
storage: options?.knownHostsStorage,
|
|
676
1052
|
storageKey: options?.knownHostsStorageKey,
|
|
@@ -678,7 +1054,7 @@ async function resolveExpectedServerIdentity(serverName, options) {
|
|
|
678
1054
|
if (stored && options?.knownHosts) {
|
|
679
1055
|
saveTrustedClientSideServerRecordToMap(options.knownHosts, stored);
|
|
680
1056
|
}
|
|
681
|
-
return stored;
|
|
1057
|
+
return stored ? normalizeExpectedIdentity(stored) : null;
|
|
682
1058
|
}
|
|
683
1059
|
async function verifyServerIdentityForAnswer(input) {
|
|
684
1060
|
const { message, serverName, connectionId, challengeNonce, expectedIdentity } = input;
|
|
@@ -689,11 +1065,11 @@ async function verifyServerIdentityForAnswer(input) {
|
|
|
689
1065
|
return;
|
|
690
1066
|
}
|
|
691
1067
|
if (message.authorityRecord && input.optionsIdentity?.authority?.publicKeyJwk) {
|
|
692
|
-
const authorityOk = await
|
|
1068
|
+
const authorityOk = await verifyAnySignedClientSideServerAuthorityRecord(message.authorityRecord, input.optionsIdentity.authority.publicKeyJwk);
|
|
693
1069
|
if (!authorityOk) {
|
|
694
1070
|
throw new Error(`Server ${serverName} provided an invalid authority record`);
|
|
695
1071
|
}
|
|
696
|
-
if (!clientSideServerPublicKeysEqual(message.authorityRecord
|
|
1072
|
+
if (!clientSideServerPublicKeysEqual(getAuthoritySigningPublicKey(message.authorityRecord), message.identity.publicKeyJwk)) {
|
|
697
1073
|
throw new Error(`Server ${serverName} authority record does not match presented identity`);
|
|
698
1074
|
}
|
|
699
1075
|
}
|
|
@@ -705,10 +1081,52 @@ async function verifyServerIdentityForAnswer(input) {
|
|
|
705
1081
|
if (!verified) {
|
|
706
1082
|
throw new Error(`Server ${serverName} failed identity challenge verification`);
|
|
707
1083
|
}
|
|
708
|
-
if (expectedIdentity && !clientSideServerPublicKeysEqual(expectedIdentity.publicKeyJwk, message.identity.publicKeyJwk)) {
|
|
1084
|
+
if (expectedIdentity && !clientSideServerPublicKeysEqual(expectedIdentity.signing.publicKeyJwk, message.identity.publicKeyJwk)) {
|
|
709
1085
|
throw new Error(`Server ${serverName} presented an unexpected public key`);
|
|
710
1086
|
}
|
|
711
1087
|
}
|
|
1088
|
+
async function verifyServerIdentityForSealedAnswer(input) {
|
|
1089
|
+
const message = {
|
|
1090
|
+
protocol: 'plat-css-v1',
|
|
1091
|
+
kind: 'answer',
|
|
1092
|
+
senderId: 'sealed',
|
|
1093
|
+
connectionId: input.connectionId,
|
|
1094
|
+
serverName: input.serverName,
|
|
1095
|
+
description: input.message.description,
|
|
1096
|
+
identity: input.message.identity,
|
|
1097
|
+
authorityRecord: input.message.authorityRecord,
|
|
1098
|
+
challengeNonce: input.message.challengeNonce,
|
|
1099
|
+
challengeSignature: input.message.challengeSignature,
|
|
1100
|
+
at: input.message.at,
|
|
1101
|
+
};
|
|
1102
|
+
await verifyServerIdentityForAnswer({
|
|
1103
|
+
...input,
|
|
1104
|
+
message,
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
async function maybeTrustServerAfterAnswer(serverName, expectedIdentity, message, identityOptions) {
|
|
1108
|
+
if (expectedIdentity)
|
|
1109
|
+
return;
|
|
1110
|
+
if (message.authorityRecord && identityOptions?.authority?.publicKeyJwk) {
|
|
1111
|
+
const trusted = await trustClientSideServerFromAuthorityRecord(message.authorityRecord, identityOptions.authority.publicKeyJwk, {
|
|
1112
|
+
storage: identityOptions?.knownHostsStorage,
|
|
1113
|
+
storageKey: identityOptions?.knownHostsStorageKey,
|
|
1114
|
+
});
|
|
1115
|
+
if (identityOptions?.knownHosts) {
|
|
1116
|
+
saveTrustedClientSideServerRecordToMap(identityOptions.knownHosts, trusted);
|
|
1117
|
+
}
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
if (identityOptions?.trustOnFirstUse !== false && message.identity) {
|
|
1121
|
+
const trusted = await trustClientSideServerOnFirstUse(serverName, message.identity, {
|
|
1122
|
+
storage: identityOptions?.knownHostsStorage,
|
|
1123
|
+
storageKey: identityOptions?.knownHostsStorageKey,
|
|
1124
|
+
});
|
|
1125
|
+
if (identityOptions?.knownHosts) {
|
|
1126
|
+
saveTrustedClientSideServerRecordToMap(identityOptions.knownHosts, trusted);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
712
1130
|
export function createClientSideServerMQTTWebRTCServer(options) {
|
|
713
1131
|
return new ClientSideServerMQTTWebRTCServer(options);
|
|
714
1132
|
}
|
|
@@ -759,6 +1177,19 @@ function publish(client, topic, message) {
|
|
|
759
1177
|
});
|
|
760
1178
|
});
|
|
761
1179
|
}
|
|
1180
|
+
function publishJson(client, topic, message) {
|
|
1181
|
+
return publish(client, topic, message);
|
|
1182
|
+
}
|
|
1183
|
+
function publishRaw(client, topic, message) {
|
|
1184
|
+
return new Promise((resolve, reject) => {
|
|
1185
|
+
client.publish(topic, message, (error) => {
|
|
1186
|
+
if (error)
|
|
1187
|
+
reject(error);
|
|
1188
|
+
else
|
|
1189
|
+
resolve();
|
|
1190
|
+
});
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
762
1193
|
function endClient(client) {
|
|
763
1194
|
return new Promise((resolve) => {
|
|
764
1195
|
client.end(false, {}, () => resolve());
|
|
@@ -786,6 +1217,350 @@ function randomId(prefix) {
|
|
|
786
1217
|
function isDefaultMQTTWebRTCOptions(options) {
|
|
787
1218
|
return Object.keys(options).length === 0;
|
|
788
1219
|
}
|
|
1220
|
+
function isSecureSignalingEnabled(options) {
|
|
1221
|
+
return options.secureSignaling !== false;
|
|
1222
|
+
}
|
|
1223
|
+
function resolvePlaintextTopic(options) {
|
|
1224
|
+
return options.mqttTopic ?? DEFAULT_CLIENT_SIDE_SERVER_MQTT_TOPIC;
|
|
1225
|
+
}
|
|
1226
|
+
function resolveSealedTopic(options) {
|
|
1227
|
+
if (isSecureSignalingEnabled(options) && options.anonymousRouting !== false) {
|
|
1228
|
+
return options.sealedTopic ?? DEFAULT_CLIENT_SIDE_SERVER_SEALED_TOPIC;
|
|
1229
|
+
}
|
|
1230
|
+
return resolvePlaintextTopic(options);
|
|
1231
|
+
}
|
|
1232
|
+
function envelopeIsReplayedOrStale(payload, options) {
|
|
1233
|
+
const replayWindowMs = options.replayWindowMs ?? DEFAULT_CLIENT_SIDE_SERVER_REPLAY_WINDOW_MS;
|
|
1234
|
+
const clockSkewToleranceMs = options.clockSkewToleranceMs ?? DEFAULT_CLIENT_SIDE_SERVER_CLOCK_SKEW_TOLERANCE_MS;
|
|
1235
|
+
const stale = Math.abs(Date.now() - payload.at) > (clockSkewToleranceMs + replayWindowMs);
|
|
1236
|
+
if (stale) {
|
|
1237
|
+
debugSecure('sealed payload stale', { type: payload.type, connectionId: payload.connectionId, at: payload.at });
|
|
1238
|
+
}
|
|
1239
|
+
return stale;
|
|
1240
|
+
}
|
|
1241
|
+
async function openClientSideServerSealedEnvelopeForServer(input) {
|
|
1242
|
+
if (!input.serverEncryptionKeyPair)
|
|
1243
|
+
return null;
|
|
1244
|
+
const envelope = parseSealedEnvelope(input.envelopePayload);
|
|
1245
|
+
if (!envelope)
|
|
1246
|
+
return null;
|
|
1247
|
+
try {
|
|
1248
|
+
const privateKey = await importX25519PrivateKeyJwk(input.serverEncryptionKeyPair.privateKeyJwk);
|
|
1249
|
+
const publicKey = await importX25519PublicKeyJwk(envelope.clientEphemeralPublicKeyJwk);
|
|
1250
|
+
const aeadKey = await deriveAeadKeyFromX25519(privateKey, publicKey, utf8('plat-css-sealed-signaling-v1'));
|
|
1251
|
+
const ciphertext = unpadCiphertext(decodeBase64Url(envelope.ciphertext));
|
|
1252
|
+
const payload = await decryptJsonAead(aeadKey, ciphertext, buildSealedEnvelopeAad(envelope), decodeBase64Url(envelope.nonce));
|
|
1253
|
+
if (!isClientSideServerSealedPayload(payload)) {
|
|
1254
|
+
debugSecure('sealed decrypt failed', { reason: 'payload-validator' });
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
return { envelope, payload };
|
|
1258
|
+
}
|
|
1259
|
+
catch (error) {
|
|
1260
|
+
debugSecure('sealed decrypt failed', { error: error instanceof Error ? error.message : String(error) });
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
async function openClientSideServerSealedEnvelopeForClient(input) {
|
|
1265
|
+
const envelope = parseSealedEnvelope(input.envelopePayload);
|
|
1266
|
+
if (!envelope)
|
|
1267
|
+
return null;
|
|
1268
|
+
try {
|
|
1269
|
+
const aeadKey = await deriveAeadKeyFromX25519(input.secureState.keyPair.privateKey, input.secureState.serverEncryptionPublicKey, utf8('plat-css-sealed-signaling-v1'));
|
|
1270
|
+
const payload = await decryptJsonAead(aeadKey, unpadCiphertext(decodeBase64Url(envelope.ciphertext)), buildSealedEnvelopeAad(envelope), decodeBase64Url(envelope.nonce));
|
|
1271
|
+
if (!isClientSideServerSealedPayload(payload))
|
|
1272
|
+
return null;
|
|
1273
|
+
return { envelope, payload };
|
|
1274
|
+
}
|
|
1275
|
+
catch {
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
async function createSecureClientSessionState(input) {
|
|
1280
|
+
const keyPair = await generateEphemeralX25519KeyPair();
|
|
1281
|
+
const serverEncryptionPublicKeyJwk = input.address.encryptionPublicKeyJwk
|
|
1282
|
+
?? input.expectedIdentity?.encryption?.publicKeyJwk;
|
|
1283
|
+
if (serverEncryptionPublicKeyJwk) {
|
|
1284
|
+
return {
|
|
1285
|
+
keyPair,
|
|
1286
|
+
serverEncryptionPublicKeyJwk,
|
|
1287
|
+
serverEncryptionPublicKey: await importX25519PublicKeyJwk(serverEncryptionPublicKeyJwk),
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
return bootstrapServerEncryptionIdentity({
|
|
1291
|
+
...input,
|
|
1292
|
+
keyPair,
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
async function bootstrapServerEncryptionIdentity(input) {
|
|
1296
|
+
const challengeNonce = randomId('enc-bootstrap');
|
|
1297
|
+
const timeoutMs = Math.min(input.options.connectionTimeoutMs ?? 10_000, input.options.workerPool?.discoveryTimeoutMs ?? 3_000);
|
|
1298
|
+
const ready = deferred();
|
|
1299
|
+
const onMessage = async (_topic, payload) => {
|
|
1300
|
+
const message = parseSignalingMessage(payload);
|
|
1301
|
+
if (!message || message.senderId === input.peerId)
|
|
1302
|
+
return;
|
|
1303
|
+
if (message.kind !== 'announce')
|
|
1304
|
+
return;
|
|
1305
|
+
if (message.serverName !== input.address.serverName)
|
|
1306
|
+
return;
|
|
1307
|
+
if (message.challengeNonce !== challengeNonce)
|
|
1308
|
+
return;
|
|
1309
|
+
if (!message.identity || !message.challengeSignature || !message.encryptionIdentity)
|
|
1310
|
+
return;
|
|
1311
|
+
if (!message.encryptionChallengeCiphertext || !message.encryptionChallengeNonce)
|
|
1312
|
+
return;
|
|
1313
|
+
if (input.expectedIdentity
|
|
1314
|
+
&& !clientSideServerPublicKeysEqual(input.expectedIdentity.signing.publicKeyJwk, message.identity.publicKeyJwk)) {
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
if (message.authorityRecord && input.options.identity?.authority?.publicKeyJwk) {
|
|
1318
|
+
const authorityOk = await verifyAnySignedClientSideServerAuthorityRecord(message.authorityRecord, input.options.identity.authority.publicKeyJwk);
|
|
1319
|
+
if (!authorityOk)
|
|
1320
|
+
return;
|
|
1321
|
+
if (isClientSideServerSignedAuthorityRecordV2(message.authorityRecord)
|
|
1322
|
+
&& !clientSideServerPublicKeysEqual(message.authorityRecord.encryptionPublicKeyJwk, message.encryptionIdentity.publicKeyJwk)) {
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
const signedChallengeOk = await verifyClientSideServerChallenge(message.identity.publicKeyJwk, buildClientSideServerIdentityChallenge({
|
|
1327
|
+
serverName: input.address.serverName,
|
|
1328
|
+
connectionId: message.senderId,
|
|
1329
|
+
challengeNonce,
|
|
1330
|
+
}), message.challengeSignature);
|
|
1331
|
+
if (!signedChallengeOk)
|
|
1332
|
+
return;
|
|
1333
|
+
try {
|
|
1334
|
+
const serverEncryptionPublicKey = await importX25519PublicKeyJwk(message.encryptionIdentity.publicKeyJwk);
|
|
1335
|
+
const aeadKey = await deriveAeadKeyFromX25519(input.keyPair.privateKey, serverEncryptionPublicKey, utf8('plat-css-sealed-signaling-v1'));
|
|
1336
|
+
const proof = await decryptJsonAead(aeadKey, decodeBase64Url(message.encryptionChallengeCiphertext), buildBootstrapEncryptionAad({
|
|
1337
|
+
senderId: message.senderId,
|
|
1338
|
+
serverName: input.address.serverName,
|
|
1339
|
+
challengeNonce,
|
|
1340
|
+
clientEphemeralPublicKeyJwk: input.keyPair.publicKeyJwk,
|
|
1341
|
+
serverEncryptionPublicKeyJwk: message.encryptionIdentity.publicKeyJwk,
|
|
1342
|
+
}), decodeBase64Url(message.encryptionChallengeNonce));
|
|
1343
|
+
if (proof.challengeNonce !== challengeNonce)
|
|
1344
|
+
return;
|
|
1345
|
+
if (proof.senderId !== message.senderId)
|
|
1346
|
+
return;
|
|
1347
|
+
if (proof.serverName !== input.address.serverName)
|
|
1348
|
+
return;
|
|
1349
|
+
ready.resolve({
|
|
1350
|
+
keyPair: input.keyPair,
|
|
1351
|
+
serverEncryptionPublicKeyJwk: message.encryptionIdentity.publicKeyJwk,
|
|
1352
|
+
serverEncryptionPublicKey,
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
catch {
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
input.mqtt.on('message', onMessage);
|
|
1360
|
+
const timer = setTimeout(() => {
|
|
1361
|
+
ready.reject(new Error(`Timed out bootstrapping encryption public key for ${input.address.serverName}`));
|
|
1362
|
+
}, timeoutMs);
|
|
1363
|
+
try {
|
|
1364
|
+
await publish(input.mqtt, resolvePlaintextTopic(input.options), {
|
|
1365
|
+
protocol: 'plat-css-v1',
|
|
1366
|
+
kind: 'discover',
|
|
1367
|
+
senderId: input.peerId,
|
|
1368
|
+
targetId: input.address.serverName,
|
|
1369
|
+
challengeNonce,
|
|
1370
|
+
requestEncryptionIdentity: true,
|
|
1371
|
+
bootstrapClientEphemeralPublicKeyJwk: input.keyPair.publicKeyJwk,
|
|
1372
|
+
at: Date.now(),
|
|
1373
|
+
});
|
|
1374
|
+
const secureState = await ready.promise;
|
|
1375
|
+
input.address.encryptionPublicKeyJwk = secureState.serverEncryptionPublicKeyJwk;
|
|
1376
|
+
return secureState;
|
|
1377
|
+
}
|
|
1378
|
+
finally {
|
|
1379
|
+
clearTimeout(timer);
|
|
1380
|
+
input.mqtt.off('message', onMessage);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
async function createPublicEncryptionBootstrapResponse(input) {
|
|
1384
|
+
if (!input.serverEncryptionKeyPair || !input.serverEncryptionIdentity || !input.challengeNonce) {
|
|
1385
|
+
return {};
|
|
1386
|
+
}
|
|
1387
|
+
const privateKey = await importX25519PrivateKeyJwk(input.serverEncryptionKeyPair.privateKeyJwk);
|
|
1388
|
+
const publicKey = await importX25519PublicKeyJwk(input.clientEphemeralPublicKeyJwk);
|
|
1389
|
+
const aeadKey = await deriveAeadKeyFromX25519(privateKey, publicKey, utf8('plat-css-sealed-signaling-v1'));
|
|
1390
|
+
const nonce = randomNonce12();
|
|
1391
|
+
const ciphertext = await encryptJsonAead(aeadKey, {
|
|
1392
|
+
challengeNonce: input.challengeNonce,
|
|
1393
|
+
senderId: input.serverInstanceId,
|
|
1394
|
+
serverName: input.serverName,
|
|
1395
|
+
}, buildBootstrapEncryptionAad({
|
|
1396
|
+
senderId: input.serverInstanceId,
|
|
1397
|
+
serverName: input.serverName,
|
|
1398
|
+
challengeNonce: input.challengeNonce,
|
|
1399
|
+
clientEphemeralPublicKeyJwk: input.clientEphemeralPublicKeyJwk,
|
|
1400
|
+
serverEncryptionPublicKeyJwk: input.serverEncryptionIdentity.publicKeyJwk,
|
|
1401
|
+
}), nonce);
|
|
1402
|
+
return {
|
|
1403
|
+
encryptionIdentity: input.serverEncryptionIdentity,
|
|
1404
|
+
encryptionChallengeCiphertext: encodeBase64Url(ciphertext),
|
|
1405
|
+
encryptionChallengeNonce: encodeBase64Url(nonce),
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
async function publishSealedClientPayload(input) {
|
|
1409
|
+
const nonce = randomNonce12();
|
|
1410
|
+
const envelope = await sealClientSideServerPayload({
|
|
1411
|
+
senderId: input.senderId,
|
|
1412
|
+
payload: input.payload,
|
|
1413
|
+
privateKey: input.secureState.keyPair.privateKey,
|
|
1414
|
+
peerPublicKey: input.secureState.serverEncryptionPublicKey,
|
|
1415
|
+
clientEphemeralPublicKeyJwk: input.secureState.keyPair.publicKeyJwk,
|
|
1416
|
+
serverEncryptionPublicKeyJwk: input.secureState.serverEncryptionPublicKeyJwk,
|
|
1417
|
+
maxSealedMessageBytes: input.options.maxSealedMessageBytes,
|
|
1418
|
+
nonce,
|
|
1419
|
+
});
|
|
1420
|
+
await publishRaw(input.mqtt, resolveSealedTopic(input.options), serializeSealedEnvelope(envelope.envelope));
|
|
1421
|
+
}
|
|
1422
|
+
async function sealClientSideServerPayloadForServer(input) {
|
|
1423
|
+
if (!input.serverEncryptionKeyPair) {
|
|
1424
|
+
throw new Error('Server encryption key pair is required for sealed signaling');
|
|
1425
|
+
}
|
|
1426
|
+
const privateKey = await importX25519PrivateKeyJwk(input.serverEncryptionKeyPair.privateKeyJwk);
|
|
1427
|
+
const publicKey = await importX25519PublicKeyJwk(input.clientEphemeralPublicKeyJwk);
|
|
1428
|
+
return {
|
|
1429
|
+
...(await sealClientSideServerPayload({
|
|
1430
|
+
senderId: input.senderId,
|
|
1431
|
+
payload: input.payload,
|
|
1432
|
+
privateKey,
|
|
1433
|
+
peerPublicKey: publicKey,
|
|
1434
|
+
clientEphemeralPublicKeyJwk: input.clientEphemeralPublicKeyJwk,
|
|
1435
|
+
serverEncryptionPublicKeyJwk: input.serverEncryptionKeyPair.publicKeyJwk,
|
|
1436
|
+
maxSealedMessageBytes: input.maxSealedMessageBytes,
|
|
1437
|
+
})),
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
async function sealClientSideServerPayload(input) {
|
|
1441
|
+
const nonceBytes = input.nonce ?? randomNonce12();
|
|
1442
|
+
const nonce = encodeBase64Url(nonceBytes);
|
|
1443
|
+
const at = Date.now();
|
|
1444
|
+
const sessionId = await computeSessionId({
|
|
1445
|
+
clientEphemeralPublicKeyJwk: input.clientEphemeralPublicKeyJwk,
|
|
1446
|
+
serverEncryptionPublicKeyJwk: input.serverEncryptionPublicKeyJwk,
|
|
1447
|
+
nonceB64u: nonce,
|
|
1448
|
+
}).catch(() => '');
|
|
1449
|
+
const aeadKey = await deriveAeadKeyFromX25519(input.privateKey, input.peerPublicKey, utf8('plat-css-sealed-signaling-v1'));
|
|
1450
|
+
const aadEnvelope = {
|
|
1451
|
+
platcss: 'sealed',
|
|
1452
|
+
version: 1,
|
|
1453
|
+
senderId: input.senderId,
|
|
1454
|
+
at,
|
|
1455
|
+
nonce,
|
|
1456
|
+
clientEphemeralPublicKeyJwk: input.clientEphemeralPublicKeyJwk,
|
|
1457
|
+
};
|
|
1458
|
+
const ciphertext = await encryptJsonAead(aeadKey, input.payload, utf8(stableJson(aadEnvelope)), nonceBytes);
|
|
1459
|
+
const bucket = choosePaddingBucket(ciphertext.byteLength);
|
|
1460
|
+
const padded = padCiphertext(ciphertext, bucket);
|
|
1461
|
+
const maxBytes = input.maxSealedMessageBytes ?? DEFAULT_CLIENT_SIDE_SERVER_MAX_SEALED_MESSAGE_BYTES;
|
|
1462
|
+
if (padded.byteLength > maxBytes) {
|
|
1463
|
+
throw new Error(`Sealed signaling message exceeds maximum size of ${maxBytes} bytes`);
|
|
1464
|
+
}
|
|
1465
|
+
return {
|
|
1466
|
+
sessionId,
|
|
1467
|
+
envelope: {
|
|
1468
|
+
...aadEnvelope,
|
|
1469
|
+
ciphertext: encodeBase64Url(padded),
|
|
1470
|
+
},
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
function parseSealedEnvelope(payload) {
|
|
1474
|
+
try {
|
|
1475
|
+
const text = payload instanceof Uint8Array ? new TextDecoder().decode(payload) : String(payload);
|
|
1476
|
+
const parsed = JSON.parse(text);
|
|
1477
|
+
if (!isClientSideServerSealedEnvelope(parsed)) {
|
|
1478
|
+
debugSecure('sealed envelope parse failed', { reason: 'validator' });
|
|
1479
|
+
return null;
|
|
1480
|
+
}
|
|
1481
|
+
return parsed;
|
|
1482
|
+
}
|
|
1483
|
+
catch {
|
|
1484
|
+
return null;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
function buildSealedEnvelopeAad(envelope) {
|
|
1488
|
+
return utf8(stableJson({
|
|
1489
|
+
platcss: 'sealed',
|
|
1490
|
+
version: 1,
|
|
1491
|
+
senderId: envelope.senderId,
|
|
1492
|
+
at: envelope.at,
|
|
1493
|
+
nonce: envelope.nonce,
|
|
1494
|
+
clientEphemeralPublicKeyJwk: envelope.clientEphemeralPublicKeyJwk,
|
|
1495
|
+
}));
|
|
1496
|
+
}
|
|
1497
|
+
function buildBootstrapEncryptionAad(input) {
|
|
1498
|
+
return utf8(stableJson({
|
|
1499
|
+
protocol: 'plat-css-v1',
|
|
1500
|
+
kind: 'announce',
|
|
1501
|
+
senderId: input.senderId,
|
|
1502
|
+
serverName: input.serverName,
|
|
1503
|
+
challengeNonce: input.challengeNonce,
|
|
1504
|
+
bootstrapClientEphemeralPublicKeyJwk: input.clientEphemeralPublicKeyJwk,
|
|
1505
|
+
encryptionPublicKeyJwk: input.serverEncryptionPublicKeyJwk,
|
|
1506
|
+
}));
|
|
1507
|
+
}
|
|
1508
|
+
function serializeSealedEnvelope(envelope) {
|
|
1509
|
+
return JSON.stringify({
|
|
1510
|
+
platcss: envelope.platcss,
|
|
1511
|
+
version: envelope.version,
|
|
1512
|
+
senderId: envelope.senderId,
|
|
1513
|
+
at: envelope.at,
|
|
1514
|
+
nonce: envelope.nonce,
|
|
1515
|
+
clientEphemeralPublicKeyJwk: envelope.clientEphemeralPublicKeyJwk,
|
|
1516
|
+
ciphertext: envelope.ciphertext,
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
function resolveAddressAuth(address) {
|
|
1520
|
+
const username = address.params.username;
|
|
1521
|
+
const password = address.params.password;
|
|
1522
|
+
if (!username || !password)
|
|
1523
|
+
return undefined;
|
|
1524
|
+
return { username, password };
|
|
1525
|
+
}
|
|
1526
|
+
function getAuthoritySigningPublicKey(record) {
|
|
1527
|
+
return isClientSideServerSignedAuthorityRecordV2(record)
|
|
1528
|
+
? record.signingPublicKeyJwk
|
|
1529
|
+
: record.publicKeyJwk;
|
|
1530
|
+
}
|
|
1531
|
+
function normalizeExpectedIdentity(record) {
|
|
1532
|
+
if (isClientSideServerTrustedServerRecordV2(record)) {
|
|
1533
|
+
return {
|
|
1534
|
+
trustedRecord: record,
|
|
1535
|
+
signing: {
|
|
1536
|
+
algorithm: 'ECDSA-P256',
|
|
1537
|
+
publicKeyJwk: record.signingPublicKeyJwk,
|
|
1538
|
+
keyId: record.signingKeyId ?? 'signing-key',
|
|
1539
|
+
fingerprint: record.signingFingerprint,
|
|
1540
|
+
},
|
|
1541
|
+
encryption: {
|
|
1542
|
+
algorithm: 'X25519',
|
|
1543
|
+
publicKeyJwk: record.encryptionPublicKeyJwk,
|
|
1544
|
+
keyId: record.encryptionKeyId ?? 'encryption-key',
|
|
1545
|
+
fingerprint: record.encryptionFingerprint,
|
|
1546
|
+
},
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
return {
|
|
1550
|
+
trustedRecord: record,
|
|
1551
|
+
signing: {
|
|
1552
|
+
algorithm: 'ECDSA-P256',
|
|
1553
|
+
publicKeyJwk: record.publicKeyJwk,
|
|
1554
|
+
keyId: record.keyId ?? 'signing-key',
|
|
1555
|
+
fingerprint: record.fingerprint,
|
|
1556
|
+
},
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
function debugSecure(event, details) {
|
|
1560
|
+
if (!globalThis.__PLAT_CSS_DEBUG_SECURE)
|
|
1561
|
+
return;
|
|
1562
|
+
console.debug(`[plat-css secure] ${event}`, details ?? {});
|
|
1563
|
+
}
|
|
789
1564
|
function deferred() {
|
|
790
1565
|
let resolve;
|
|
791
1566
|
let reject;
|