@peers-app/peers-sdk 0.20.5 → 0.21.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/data/devices.d.ts +4 -4
- package/dist/data/voice-messages.d.ts +2 -2
- package/dist/device/binary-peer-connection-v2.d.ts +19 -2
- package/dist/device/binary-peer-connection-v2.js +127 -20
- package/dist/device/binary-peer-connection-v2.test.js +60 -0
- package/dist/device/binary-peer-connection.d.ts +13 -2
- package/dist/device/binary-peer-connection.js +77 -14
- package/dist/device/binary-peer-connection.test.js +21 -4
- package/dist/device/connection.d.ts +1 -1
- package/dist/device/device.d.ts +1 -1
- package/dist/device-pairing/constants.d.ts +34 -0
- package/dist/device-pairing/constants.js +37 -0
- package/dist/device-pairing/crypto.d.ts +45 -0
- package/dist/device-pairing/crypto.js +135 -0
- package/dist/device-pairing/device-pairing.test.d.ts +1 -0
- package/dist/device-pairing/device-pairing.test.js +174 -0
- package/dist/device-pairing/index.d.ts +6 -0
- package/dist/device-pairing/index.js +22 -0
- package/dist/device-pairing/invitation.d.ts +13 -0
- package/dist/device-pairing/invitation.js +85 -0
- package/dist/device-pairing/signaling.d.ts +9 -0
- package/dist/device-pairing/signaling.js +29 -0
- package/dist/device-pairing/transcript.d.ts +7 -0
- package/dist/device-pairing/transcript.js +38 -0
- package/dist/device-pairing/types.d.ts +1023 -0
- package/dist/device-pairing/types.js +304 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/rpc-types.d.ts +8 -0
- package/dist/rpc-types.js +7 -0
- package/dist/types/peer-device.d.ts +38 -1
- package/dist/types/peer-device.js +1 -0
- package/package.json +1 -1
|
@@ -152,16 +152,27 @@ function decodeMessage(bytes) {
|
|
|
152
152
|
}
|
|
153
153
|
return message;
|
|
154
154
|
}
|
|
155
|
+
const DEFAULT_RECEIVE_LIMITS = Object.freeze({
|
|
156
|
+
maxRpcMessageBytes: 256 * 1024 * 1024,
|
|
157
|
+
maxStreamMessageBytes: 256 * 1024 * 1024,
|
|
158
|
+
reassemblyTimeoutMs: 30_000,
|
|
159
|
+
});
|
|
155
160
|
/**
|
|
156
161
|
* Creates an ISocket from a binary peer without creating a full Connection.
|
|
157
162
|
* Useful when you need to pass custom localServerAddresses to the Connection.
|
|
158
163
|
*/
|
|
159
|
-
function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
|
|
164
|
+
function createBinaryPeerSocket(connectionId, peer, protocol = "binary", receiveLimitOverrides = {}) {
|
|
160
165
|
const handlers = {};
|
|
161
166
|
const callbacks = {};
|
|
162
167
|
const rawBytesHandlers = {};
|
|
163
168
|
let isClosed = false;
|
|
164
169
|
const stats = (0, streamed_socket_1.createSocketStats)();
|
|
170
|
+
const receiveLimits = { ...DEFAULT_RECEIVE_LIMITS, ...receiveLimitOverrides };
|
|
171
|
+
for (const [name, value] of Object.entries(receiveLimits)) {
|
|
172
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
173
|
+
throw new Error(`Invalid binary peer receive limit ${name}: ${value}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
165
176
|
// Map protocol string to TransportType (needed early for threshold calculations)
|
|
166
177
|
const transportType = protocol === "wrtc" ? "wrtc" : protocol === "ws" ? "ws" : "unknown";
|
|
167
178
|
// Transport-specific thresholds (computed once)
|
|
@@ -246,6 +257,25 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
|
|
|
246
257
|
// Reassembly state for chunked __rpc messages
|
|
247
258
|
let rpcReassemblyBuffer = null;
|
|
248
259
|
let rpcReassemblyOffset = 0;
|
|
260
|
+
let rpcReassemblyTimer;
|
|
261
|
+
function clearRpcReassembly() {
|
|
262
|
+
if (rpcReassemblyTimer) {
|
|
263
|
+
clearTimeout(rpcReassemblyTimer);
|
|
264
|
+
rpcReassemblyTimer = undefined;
|
|
265
|
+
}
|
|
266
|
+
rpcReassemblyBuffer = null;
|
|
267
|
+
rpcReassemblyOffset = 0;
|
|
268
|
+
}
|
|
269
|
+
function closeForProtocolViolation(reason) {
|
|
270
|
+
if (isClosed) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
console.warn(`Closing ${protocol} peer after binary protocol violation: ${reason}`);
|
|
274
|
+
isClosed = true;
|
|
275
|
+
clearRpcReassembly();
|
|
276
|
+
socket.connected = false;
|
|
277
|
+
peer.destroy();
|
|
278
|
+
}
|
|
249
279
|
// Handle incoming data
|
|
250
280
|
peer.on("data", async (data) => {
|
|
251
281
|
try {
|
|
@@ -266,6 +296,10 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
|
|
|
266
296
|
const streamIdBytes = bytes.subarray(3, 3 + streamIdLength);
|
|
267
297
|
const streamId = decodeStreamId(streamIdBytes);
|
|
268
298
|
const payload = bytes.subarray(3 + streamIdLength);
|
|
299
|
+
if (payload.length > receiveLimits.maxStreamMessageBytes) {
|
|
300
|
+
closeForProtocolViolation("raw stream frame exceeds the message-size limit");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
269
303
|
// Check if this is an RPC message sent via raw bytes (for large payloads)
|
|
270
304
|
if (streamId === RPC_STREAM_ID) {
|
|
271
305
|
if (payload.length === 0)
|
|
@@ -273,6 +307,10 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
|
|
|
273
307
|
const flag = payload[0];
|
|
274
308
|
if (flag === RPC_FLAG_SINGLE) {
|
|
275
309
|
// Complete message in one raw bytes send
|
|
310
|
+
if (payload.length - 1 > receiveLimits.maxRpcMessageBytes) {
|
|
311
|
+
closeForProtocolViolation("complete RPC exceeds the message-size limit");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
276
314
|
handleRPCMessage(payload.subarray(1), handlers, callbacks, sendRPCData);
|
|
277
315
|
}
|
|
278
316
|
else if (flag === RPC_FLAG_START) {
|
|
@@ -280,16 +318,24 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
|
|
|
280
318
|
if (payload.length < 5)
|
|
281
319
|
return; // flag + 4 bytes size minimum
|
|
282
320
|
const totalSize = new DataView(payload.buffer, payload.byteOffset + 1, 4).getUint32(0, false);
|
|
321
|
+
const chunkData = payload.subarray(5);
|
|
322
|
+
if (totalSize === 0 ||
|
|
323
|
+
totalSize > receiveLimits.maxRpcMessageBytes ||
|
|
324
|
+
chunkData.length > totalSize ||
|
|
325
|
+
rpcReassemblyBuffer) {
|
|
326
|
+
closeForProtocolViolation("invalid, duplicate, or oversized RPC start frame");
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
283
329
|
rpcReassemblyBuffer = new Uint8Array(totalSize);
|
|
284
330
|
rpcReassemblyOffset = 0;
|
|
285
|
-
const chunkData = payload.subarray(5);
|
|
286
331
|
rpcReassemblyBuffer.set(chunkData, 0);
|
|
287
332
|
rpcReassemblyOffset = chunkData.length;
|
|
333
|
+
rpcReassemblyTimer = setTimeout(() => closeForProtocolViolation("RPC reassembly timed out"), receiveLimits.reassemblyTimeoutMs);
|
|
334
|
+
rpcReassemblyTimer.unref?.();
|
|
288
335
|
// Check if this single START chunk completes the message (unlikely but safe)
|
|
289
|
-
if (rpcReassemblyOffset
|
|
336
|
+
if (rpcReassemblyOffset === totalSize) {
|
|
290
337
|
handleRPCMessage(rpcReassemblyBuffer, handlers, callbacks, sendRPCData);
|
|
291
|
-
|
|
292
|
-
rpcReassemblyOffset = 0;
|
|
338
|
+
clearRpcReassembly();
|
|
293
339
|
}
|
|
294
340
|
}
|
|
295
341
|
else if (flag === RPC_FLAG_CONT) {
|
|
@@ -297,17 +343,24 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
|
|
|
297
343
|
if (!rpcReassemblyBuffer)
|
|
298
344
|
return; // no START received, discard
|
|
299
345
|
const chunkData = payload.subarray(1);
|
|
346
|
+
if (chunkData.length > rpcReassemblyBuffer.length - rpcReassemblyOffset) {
|
|
347
|
+
closeForProtocolViolation("RPC continuation exceeds declared size");
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
300
350
|
rpcReassemblyBuffer.set(chunkData, rpcReassemblyOffset);
|
|
301
351
|
rpcReassemblyOffset += chunkData.length;
|
|
302
|
-
if (rpcReassemblyOffset
|
|
352
|
+
if (rpcReassemblyOffset === rpcReassemblyBuffer.length) {
|
|
303
353
|
handleRPCMessage(rpcReassemblyBuffer, handlers, callbacks, sendRPCData);
|
|
304
|
-
|
|
305
|
-
rpcReassemblyOffset = 0;
|
|
354
|
+
clearRpcReassembly();
|
|
306
355
|
}
|
|
307
356
|
}
|
|
308
357
|
else {
|
|
309
358
|
// Legacy/unknown: treat entire payload as a complete RPC message
|
|
310
359
|
// (backwards compat with code before chunking was added)
|
|
360
|
+
if (payload.length > receiveLimits.maxRpcMessageBytes) {
|
|
361
|
+
closeForProtocolViolation("legacy RPC exceeds the message-size limit");
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
311
364
|
handleRPCMessage(payload, handlers, callbacks, sendRPCData);
|
|
312
365
|
}
|
|
313
366
|
return;
|
|
@@ -320,10 +373,18 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
|
|
|
320
373
|
// Handle RPC messages
|
|
321
374
|
if (protocolType === PROTOCOL_RPC) {
|
|
322
375
|
const rpcBytes = bytes.subarray(1);
|
|
376
|
+
if (rpcBytes.length > receiveLimits.maxRpcMessageBytes) {
|
|
377
|
+
closeForProtocolViolation("complete RPC exceeds the message-size limit");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
323
380
|
handleRPCMessage(rpcBytes, handlers, callbacks, sendRPCData);
|
|
324
381
|
return;
|
|
325
382
|
}
|
|
326
383
|
// Legacy format (no discriminator) for backwards compatibility
|
|
384
|
+
if (bytes.length > receiveLimits.maxRpcMessageBytes) {
|
|
385
|
+
closeForProtocolViolation("legacy RPC exceeds the message-size limit");
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
327
388
|
handleRPCMessage(bytes, handlers, callbacks, sendRPCData);
|
|
328
389
|
}
|
|
329
390
|
catch (err) {
|
|
@@ -335,6 +396,7 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary") {
|
|
|
335
396
|
if (isClosed)
|
|
336
397
|
return;
|
|
337
398
|
isClosed = true;
|
|
399
|
+
clearRpcReassembly();
|
|
338
400
|
socket.connected = false;
|
|
339
401
|
});
|
|
340
402
|
const socket = {
|
|
@@ -389,9 +451,10 @@ function handleRPCMessage(bytes, handlers, callbacks, sendRPCData) {
|
|
|
389
451
|
return;
|
|
390
452
|
}
|
|
391
453
|
if (message.type === "call") {
|
|
392
|
-
const
|
|
454
|
+
const eventName = message.eventName;
|
|
455
|
+
const handler = eventName ? handlers[eventName] : undefined;
|
|
393
456
|
if (!handler) {
|
|
394
|
-
console.warn(`No handler for event: ${
|
|
457
|
+
console.warn(eventName ? `No handler for event: ${eventName}` : "RPC call did not include an event name");
|
|
395
458
|
return;
|
|
396
459
|
}
|
|
397
460
|
try {
|
|
@@ -427,9 +490,9 @@ function handleRPCMessage(bytes, handlers, callbacks, sendRPCData) {
|
|
|
427
490
|
* This is the main implementation that handles all binary protocols.
|
|
428
491
|
*/
|
|
429
492
|
function wrapBinaryPeer(connectionId, peer, localDevice, initiator, options, getTrustLevel) {
|
|
430
|
-
const { protocol, markTransportSecure } = options;
|
|
493
|
+
const { protocol, markTransportSecure, receiveLimits } = options;
|
|
431
494
|
// Create the socket wrapper using shared implementation
|
|
432
|
-
const socket = createBinaryPeerSocket(connectionId, peer, protocol);
|
|
495
|
+
const socket = createBinaryPeerSocket(connectionId, peer, protocol, receiveLimits);
|
|
433
496
|
const serverAddress = `${protocol}://${connectionId}`;
|
|
434
497
|
const localServerAddresses = initiator ? undefined : [serverAddress];
|
|
435
498
|
if (process.env.NODE_ENV !== "test") {
|
|
@@ -448,6 +511,6 @@ function wrapBinaryPeer(connectionId, peer, localDevice, initiator, options, get
|
|
|
448
511
|
* Backwards-compatible wrapper around wrapBinaryPeer.
|
|
449
512
|
* WebRTC is encrypted at the transport layer (DTLS-SRTP), so we skip application-level encryption.
|
|
450
513
|
*/
|
|
451
|
-
function wrapWrtc(connectionId, peer, localDevice, initiator, getTrustLevel) {
|
|
452
|
-
return wrapBinaryPeer(connectionId, peer, localDevice, initiator, { protocol: "wrtc", markTransportSecure: true }, getTrustLevel);
|
|
514
|
+
function wrapWrtc(connectionId, peer, localDevice, initiator, getTrustLevel, receiveLimits) {
|
|
515
|
+
return wrapBinaryPeer(connectionId, peer, localDevice, initiator, { protocol: "wrtc", markTransportSecure: true, receiveLimits }, getTrustLevel);
|
|
453
516
|
}
|
|
@@ -8,8 +8,8 @@ const device_1 = require("./device");
|
|
|
8
8
|
class MockBinaryPeer {
|
|
9
9
|
dataHandler;
|
|
10
10
|
closeHandler;
|
|
11
|
-
drainHandler;
|
|
12
11
|
otherPeer;
|
|
12
|
+
destroyed = false;
|
|
13
13
|
on(event, handler) {
|
|
14
14
|
if (event === "data") {
|
|
15
15
|
this.dataHandler = handler;
|
|
@@ -17,9 +17,6 @@ class MockBinaryPeer {
|
|
|
17
17
|
else if (event === "close") {
|
|
18
18
|
this.closeHandler = handler;
|
|
19
19
|
}
|
|
20
|
-
else if (event === "drain") {
|
|
21
|
-
this.drainHandler = handler;
|
|
22
|
-
}
|
|
23
20
|
}
|
|
24
21
|
send(data) {
|
|
25
22
|
if (this.otherPeer?.dataHandler) {
|
|
@@ -35,11 +32,15 @@ class MockBinaryPeer {
|
|
|
35
32
|
}
|
|
36
33
|
}
|
|
37
34
|
destroy() {
|
|
35
|
+
this.destroyed = true;
|
|
38
36
|
this.dataHandler = undefined;
|
|
39
37
|
if (this.closeHandler) {
|
|
40
38
|
this.closeHandler();
|
|
41
39
|
}
|
|
42
40
|
}
|
|
41
|
+
receive(data) {
|
|
42
|
+
this.dataHandler?.(data);
|
|
43
|
+
}
|
|
43
44
|
}
|
|
44
45
|
async function getMockPeerPair() {
|
|
45
46
|
const iPeer = new MockBinaryPeer();
|
|
@@ -75,6 +76,22 @@ describe("binary-peer-connection", () => {
|
|
|
75
76
|
rPeer.destroy();
|
|
76
77
|
});
|
|
77
78
|
});
|
|
79
|
+
it("closes the legacy parser before allocating an oversized declared RPC", async () => {
|
|
80
|
+
const { iPeer } = await getMockPeerPair();
|
|
81
|
+
(0, binary_peer_connection_1.createBinaryPeerSocket)("limited", iPeer, "wrtc", {
|
|
82
|
+
maxRpcMessageBytes: 32,
|
|
83
|
+
});
|
|
84
|
+
const streamId = new TextEncoder().encode("__rpc");
|
|
85
|
+
const frame = new Uint8Array(3 + streamId.length + 5);
|
|
86
|
+
frame[0] = 0x01;
|
|
87
|
+
const view = new DataView(frame.buffer);
|
|
88
|
+
view.setUint16(1, streamId.length, false);
|
|
89
|
+
frame.set(streamId, 3);
|
|
90
|
+
frame[3 + streamId.length] = 0x01;
|
|
91
|
+
view.setUint32(4 + streamId.length, 33, false);
|
|
92
|
+
iPeer.receive(frame);
|
|
93
|
+
expect(iPeer.destroyed).toBe(true);
|
|
94
|
+
});
|
|
78
95
|
describe("wrapBinaryPeer", () => {
|
|
79
96
|
it("should successfully handshake over wrtc protocol", async () => {
|
|
80
97
|
const { iPeer, rPeer } = await getMockPeerPair();
|
package/dist/device/device.d.ts
CHANGED
|
@@ -37,8 +37,8 @@ export declare class Device implements IDeviceInfo {
|
|
|
37
37
|
deviceId: string;
|
|
38
38
|
connectionId: string;
|
|
39
39
|
serverAddress: string;
|
|
40
|
-
userName?: string | undefined;
|
|
41
40
|
deviceName?: string | undefined;
|
|
41
|
+
userName?: string | undefined;
|
|
42
42
|
};
|
|
43
43
|
private deviceInfoSigned;
|
|
44
44
|
/** Returns cached signed identity information, refreshing after display metadata changes. */
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Wire-protocol version for the temporary device-pairing ceremony. */
|
|
2
|
+
export declare const DEVICE_PAIRING_PROTOCOL_VERSION: 1;
|
|
3
|
+
/** Maximum lifetime of a pairing invitation and signaling room. */
|
|
4
|
+
export declare const DEVICE_PAIRING_TTL_MS: number;
|
|
5
|
+
/** Socket.IO namespace reserved for device-pairing signaling. */
|
|
6
|
+
export declare const DEVICE_PAIRING_SIGNALING_NAMESPACE = "/device-pairing-v1";
|
|
7
|
+
/** Dedicated Socket.IO engine path with a pairing-sized transport buffer. */
|
|
8
|
+
export declare const DEVICE_PAIRING_SIGNALING_PATH = "/device-pairing-v1/socket.io";
|
|
9
|
+
/** Socket.IO event names used by the bounded signaling room. */
|
|
10
|
+
export declare const DEVICE_PAIRING_SIGNALING_EVENTS: {
|
|
11
|
+
readonly createRoom: "pairing:create-room";
|
|
12
|
+
readonly joinRoom: "pairing:join-room";
|
|
13
|
+
readonly getIceServers: "pairing:get-ice-servers";
|
|
14
|
+
readonly signal: "pairing:signal";
|
|
15
|
+
readonly deleteRoom: "pairing:delete-room";
|
|
16
|
+
readonly peerJoined: "pairing:peer-joined";
|
|
17
|
+
readonly peerLeft: "pairing:peer-left";
|
|
18
|
+
};
|
|
19
|
+
/** Maximum UTF-8 bytes accepted for a serialized signaling envelope. */
|
|
20
|
+
export declare const DEVICE_PAIRING_MAX_SIGNAL_BYTES: number;
|
|
21
|
+
/** Maximum packet size accepted by the anonymous pairing Socket.IO engine. */
|
|
22
|
+
export declare const DEVICE_PAIRING_MAX_TRANSPORT_BYTES: number;
|
|
23
|
+
/** Maximum ICE candidates accepted from either endpoint. */
|
|
24
|
+
export declare const DEVICE_PAIRING_MAX_ICE_CANDIDATES_PER_ROLE = 64;
|
|
25
|
+
/** Maximum tolerated source/destination wall-clock difference. */
|
|
26
|
+
export declare const DEVICE_PAIRING_CLOCK_SKEW_MS: number;
|
|
27
|
+
/** Event name used to publish renderer-safe pairing state. */
|
|
28
|
+
export declare const DEVICE_PAIRING_STATE_EVENT = "device-pairing:state";
|
|
29
|
+
/** Domain-separation label for the room bearer credential. */
|
|
30
|
+
export declare const DEVICE_PAIRING_ROOM_ACCESS_LABEL = "peers-pairing-room-access-v1";
|
|
31
|
+
/** Domain-separation label for signaling message authentication. */
|
|
32
|
+
export declare const DEVICE_PAIRING_SIGNALING_LABEL = "peers-pairing-signaling-v1";
|
|
33
|
+
/** Domain-separation label for the six-digit short authentication string. */
|
|
34
|
+
export declare const DEVICE_PAIRING_SAS_LABEL = "peers-pairing-sas-v1";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEVICE_PAIRING_SAS_LABEL = exports.DEVICE_PAIRING_SIGNALING_LABEL = exports.DEVICE_PAIRING_ROOM_ACCESS_LABEL = exports.DEVICE_PAIRING_STATE_EVENT = exports.DEVICE_PAIRING_CLOCK_SKEW_MS = exports.DEVICE_PAIRING_MAX_ICE_CANDIDATES_PER_ROLE = exports.DEVICE_PAIRING_MAX_TRANSPORT_BYTES = exports.DEVICE_PAIRING_MAX_SIGNAL_BYTES = exports.DEVICE_PAIRING_SIGNALING_EVENTS = exports.DEVICE_PAIRING_SIGNALING_PATH = exports.DEVICE_PAIRING_SIGNALING_NAMESPACE = exports.DEVICE_PAIRING_TTL_MS = exports.DEVICE_PAIRING_PROTOCOL_VERSION = void 0;
|
|
4
|
+
/** Wire-protocol version for the temporary device-pairing ceremony. */
|
|
5
|
+
exports.DEVICE_PAIRING_PROTOCOL_VERSION = 1;
|
|
6
|
+
/** Maximum lifetime of a pairing invitation and signaling room. */
|
|
7
|
+
exports.DEVICE_PAIRING_TTL_MS = 5 * 60 * 1000;
|
|
8
|
+
/** Socket.IO namespace reserved for device-pairing signaling. */
|
|
9
|
+
exports.DEVICE_PAIRING_SIGNALING_NAMESPACE = "/device-pairing-v1";
|
|
10
|
+
/** Dedicated Socket.IO engine path with a pairing-sized transport buffer. */
|
|
11
|
+
exports.DEVICE_PAIRING_SIGNALING_PATH = "/device-pairing-v1/socket.io";
|
|
12
|
+
/** Socket.IO event names used by the bounded signaling room. */
|
|
13
|
+
exports.DEVICE_PAIRING_SIGNALING_EVENTS = {
|
|
14
|
+
createRoom: "pairing:create-room",
|
|
15
|
+
joinRoom: "pairing:join-room",
|
|
16
|
+
getIceServers: "pairing:get-ice-servers",
|
|
17
|
+
signal: "pairing:signal",
|
|
18
|
+
deleteRoom: "pairing:delete-room",
|
|
19
|
+
peerJoined: "pairing:peer-joined",
|
|
20
|
+
peerLeft: "pairing:peer-left",
|
|
21
|
+
};
|
|
22
|
+
/** Maximum UTF-8 bytes accepted for a serialized signaling envelope. */
|
|
23
|
+
exports.DEVICE_PAIRING_MAX_SIGNAL_BYTES = 72 * 1024;
|
|
24
|
+
/** Maximum packet size accepted by the anonymous pairing Socket.IO engine. */
|
|
25
|
+
exports.DEVICE_PAIRING_MAX_TRANSPORT_BYTES = 96 * 1024;
|
|
26
|
+
/** Maximum ICE candidates accepted from either endpoint. */
|
|
27
|
+
exports.DEVICE_PAIRING_MAX_ICE_CANDIDATES_PER_ROLE = 64;
|
|
28
|
+
/** Maximum tolerated source/destination wall-clock difference. */
|
|
29
|
+
exports.DEVICE_PAIRING_CLOCK_SKEW_MS = 2 * 60 * 1000;
|
|
30
|
+
/** Event name used to publish renderer-safe pairing state. */
|
|
31
|
+
exports.DEVICE_PAIRING_STATE_EVENT = "device-pairing:state";
|
|
32
|
+
/** Domain-separation label for the room bearer credential. */
|
|
33
|
+
exports.DEVICE_PAIRING_ROOM_ACCESS_LABEL = "peers-pairing-room-access-v1";
|
|
34
|
+
/** Domain-separation label for signaling message authentication. */
|
|
35
|
+
exports.DEVICE_PAIRING_SIGNALING_LABEL = "peers-pairing-signaling-v1";
|
|
36
|
+
/** Domain-separation label for the six-digit short authentication string. */
|
|
37
|
+
exports.DEVICE_PAIRING_SAS_LABEL = "peers-pairing-sas-v1";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { IDeviceInfo } from "../data/devices";
|
|
2
|
+
import type { Device } from "../device/device";
|
|
3
|
+
import { type PairingCredentialContents, type PairingRole, type SignedPairingApproval, type SignedPairingInstallationReceipt, type UnsignedPairingSignal } from "./types";
|
|
4
|
+
/** Derives the high-entropy bearer credential presented when joining a room. */
|
|
5
|
+
export declare function derivePairingRoomAccessToken(roomSecret: string): string;
|
|
6
|
+
/** Hashes a room bearer credential before the source gives its verifier to the service. */
|
|
7
|
+
export declare function hashPairingRoomAccessToken(roomAccessToken: string): string;
|
|
8
|
+
/** Derives the client-only key used to authenticate SDP and ICE messages. */
|
|
9
|
+
export declare function derivePairingSignalingKey(roomSecret: string): string;
|
|
10
|
+
/** Computes the MAC over the canonical unsigned signaling envelope. */
|
|
11
|
+
export declare function computePairingSignalMac(signal: UnsignedPairingSignal, signalingKey: string): string;
|
|
12
|
+
/** Verifies a signaling MAC without short-circuiting on the first differing byte. */
|
|
13
|
+
export declare function isPairingSignalMacValid(signal: UnsignedPairingSignal, mac: string, signalingKey: string): boolean;
|
|
14
|
+
/** Creates a signed, expiring approval for one pairing transcript. */
|
|
15
|
+
export declare function createSignedPairingApproval(device: Device, role: PairingRole, transcriptHash: string, expiresAt: number): SignedPairingApproval;
|
|
16
|
+
/** Verifies an approval's signature, endpoint identity, transcript, role, and lifetime. */
|
|
17
|
+
export declare function verifySignedPairingApproval(approval: SignedPairingApproval, expected: {
|
|
18
|
+
publicKey: string;
|
|
19
|
+
transcriptHash: string;
|
|
20
|
+
role: PairingRole;
|
|
21
|
+
now?: number;
|
|
22
|
+
}): string;
|
|
23
|
+
/** Signs and encrypts account credentials directly to the temporary destination identity. */
|
|
24
|
+
export declare function createPairingCredentialEnvelope(sourceDevice: Device, destinationDevice: IDeviceInfo, credentials: {
|
|
25
|
+
userId: string;
|
|
26
|
+
secretKey: string;
|
|
27
|
+
}, transcriptHash: string, expiresAt: number): {
|
|
28
|
+
nonce: string;
|
|
29
|
+
contents: string;
|
|
30
|
+
fromPublicKey: string;
|
|
31
|
+
};
|
|
32
|
+
/** Opens and validates credentials on the temporary destination endpoint. */
|
|
33
|
+
export declare function openPairingCredentialEnvelope(destinationDevice: Device, envelope: unknown, expected: {
|
|
34
|
+
sourcePublicKey: string;
|
|
35
|
+
sourcePublicBoxKey: string;
|
|
36
|
+
transcriptHash: string;
|
|
37
|
+
now?: number;
|
|
38
|
+
}): PairingCredentialContents;
|
|
39
|
+
/** Creates the temporary destination identity's receipt after normal runtime initialization. */
|
|
40
|
+
export declare function createSignedPairingInstallationReceipt(temporaryDestinationDevice: Device, transcriptHash: string, initializedDevice: IDeviceInfo, installedAt?: number): SignedPairingInstallationReceipt;
|
|
41
|
+
/** Verifies that an installation receipt came from the authenticated temporary destination. */
|
|
42
|
+
export declare function verifySignedPairingInstallationReceipt(receipt: SignedPairingInstallationReceipt, expected: {
|
|
43
|
+
temporaryDestinationPublicKey: string;
|
|
44
|
+
transcriptHash: string;
|
|
45
|
+
}): IDeviceInfo;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.derivePairingRoomAccessToken = derivePairingRoomAccessToken;
|
|
4
|
+
exports.hashPairingRoomAccessToken = hashPairingRoomAccessToken;
|
|
5
|
+
exports.derivePairingSignalingKey = derivePairingSignalingKey;
|
|
6
|
+
exports.computePairingSignalMac = computePairingSignalMac;
|
|
7
|
+
exports.isPairingSignalMacValid = isPairingSignalMacValid;
|
|
8
|
+
exports.createSignedPairingApproval = createSignedPairingApproval;
|
|
9
|
+
exports.verifySignedPairingApproval = verifySignedPairingApproval;
|
|
10
|
+
exports.createPairingCredentialEnvelope = createPairingCredentialEnvelope;
|
|
11
|
+
exports.openPairingCredentialEnvelope = openPairingCredentialEnvelope;
|
|
12
|
+
exports.createSignedPairingInstallationReceipt = createSignedPairingInstallationReceipt;
|
|
13
|
+
exports.verifySignedPairingInstallationReceipt = verifySignedPairingInstallationReceipt;
|
|
14
|
+
const hmac_1 = require("@noble/hashes/hmac");
|
|
15
|
+
const sha2_1 = require("@noble/hashes/sha2");
|
|
16
|
+
const keys_1 = require("../keys");
|
|
17
|
+
const constants_1 = require("./constants");
|
|
18
|
+
const types_1 = require("./types");
|
|
19
|
+
const textEncoder = new TextEncoder();
|
|
20
|
+
function computeHmac(key, value) {
|
|
21
|
+
return (0, hmac_1.hmac)(sha2_1.sha256, key, textEncoder.encode(value));
|
|
22
|
+
}
|
|
23
|
+
function equalBytes(left, right) {
|
|
24
|
+
if (left.length !== right.length) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
let difference = 0;
|
|
28
|
+
for (let index = 0; index < left.length; index++) {
|
|
29
|
+
difference |= left[index] ^ right[index];
|
|
30
|
+
}
|
|
31
|
+
return difference === 0;
|
|
32
|
+
}
|
|
33
|
+
/** Derives the high-entropy bearer credential presented when joining a room. */
|
|
34
|
+
function derivePairingRoomAccessToken(roomSecret) {
|
|
35
|
+
return (0, keys_1.encodeBase64)(computeHmac((0, keys_1.decodeBase64)(roomSecret), constants_1.DEVICE_PAIRING_ROOM_ACCESS_LABEL));
|
|
36
|
+
}
|
|
37
|
+
/** Hashes a room bearer credential before the source gives its verifier to the service. */
|
|
38
|
+
function hashPairingRoomAccessToken(roomAccessToken) {
|
|
39
|
+
return (0, keys_1.hashValue)(roomAccessToken);
|
|
40
|
+
}
|
|
41
|
+
/** Derives the client-only key used to authenticate SDP and ICE messages. */
|
|
42
|
+
function derivePairingSignalingKey(roomSecret) {
|
|
43
|
+
return (0, keys_1.encodeBase64)(computeHmac((0, keys_1.decodeBase64)(roomSecret), constants_1.DEVICE_PAIRING_SIGNALING_LABEL));
|
|
44
|
+
}
|
|
45
|
+
/** Computes the MAC over the canonical unsigned signaling envelope. */
|
|
46
|
+
function computePairingSignalMac(signal, signalingKey) {
|
|
47
|
+
return (0, keys_1.encodeBase64)(computeHmac((0, keys_1.decodeBase64)(signalingKey), (0, keys_1.stableStringify)(signal)));
|
|
48
|
+
}
|
|
49
|
+
/** Verifies a signaling MAC without short-circuiting on the first differing byte. */
|
|
50
|
+
function isPairingSignalMacValid(signal, mac, signalingKey) {
|
|
51
|
+
try {
|
|
52
|
+
const expected = (0, keys_1.decodeBase64)(computePairingSignalMac(signal, signalingKey));
|
|
53
|
+
return equalBytes(expected, (0, keys_1.decodeBase64)(mac));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Creates a signed, expiring approval for one pairing transcript. */
|
|
60
|
+
function createSignedPairingApproval(device, role, transcriptHash, expiresAt) {
|
|
61
|
+
const contents = types_1.pairingApprovalContentsSchema.parse({
|
|
62
|
+
version: constants_1.DEVICE_PAIRING_PROTOCOL_VERSION,
|
|
63
|
+
approvalId: (0, keys_1.newToken)(32),
|
|
64
|
+
transcriptHash,
|
|
65
|
+
role,
|
|
66
|
+
expiresAt,
|
|
67
|
+
});
|
|
68
|
+
return types_1.signedPairingApprovalSchema.parse(device.signObjectWithSecretKey(contents));
|
|
69
|
+
}
|
|
70
|
+
/** Verifies an approval's signature, endpoint identity, transcript, role, and lifetime. */
|
|
71
|
+
function verifySignedPairingApproval(approval, expected) {
|
|
72
|
+
const parsed = types_1.signedPairingApprovalSchema.parse(approval);
|
|
73
|
+
if (parsed.publicKey !== expected.publicKey) {
|
|
74
|
+
throw new Error("Pairing approval came from an unexpected device");
|
|
75
|
+
}
|
|
76
|
+
const contents = types_1.pairingApprovalContentsSchema.parse((0, keys_1.openSignedObject)(parsed));
|
|
77
|
+
if (contents.transcriptHash !== expected.transcriptHash || contents.role !== expected.role) {
|
|
78
|
+
throw new Error("Pairing approval does not match this ceremony");
|
|
79
|
+
}
|
|
80
|
+
if (contents.expiresAt < (expected.now ?? Date.now())) {
|
|
81
|
+
throw new Error("Pairing approval has expired");
|
|
82
|
+
}
|
|
83
|
+
return contents.approvalId;
|
|
84
|
+
}
|
|
85
|
+
/** Signs and encrypts account credentials directly to the temporary destination identity. */
|
|
86
|
+
function createPairingCredentialEnvelope(sourceDevice, destinationDevice, credentials, transcriptHash, expiresAt) {
|
|
87
|
+
const contents = types_1.pairingCredentialContentsSchema.parse({
|
|
88
|
+
version: constants_1.DEVICE_PAIRING_PROTOCOL_VERSION,
|
|
89
|
+
transcriptHash,
|
|
90
|
+
expiresAt,
|
|
91
|
+
...credentials,
|
|
92
|
+
});
|
|
93
|
+
return types_1.pairingCredentialEnvelopeSchema.parse(sourceDevice.signAndBoxDataForDevice(contents, destinationDevice));
|
|
94
|
+
}
|
|
95
|
+
/** Opens and validates credentials on the temporary destination endpoint. */
|
|
96
|
+
function openPairingCredentialEnvelope(destinationDevice, envelope, expected) {
|
|
97
|
+
const parsedEnvelope = types_1.pairingCredentialEnvelopeSchema.parse(envelope);
|
|
98
|
+
if (parsedEnvelope.fromPublicKey !== expected.sourcePublicBoxKey) {
|
|
99
|
+
throw new Error("Pairing credentials came from an unexpected device");
|
|
100
|
+
}
|
|
101
|
+
const signedContents = destinationDevice.openBoxWithSecretKey(parsedEnvelope);
|
|
102
|
+
if (signedContents.publicKey !== expected.sourcePublicKey) {
|
|
103
|
+
throw new Error("Pairing credentials were signed by an unexpected device");
|
|
104
|
+
}
|
|
105
|
+
const contents = types_1.pairingCredentialContentsSchema.parse((0, keys_1.openSignedObject)(signedContents));
|
|
106
|
+
if (contents.transcriptHash !== expected.transcriptHash) {
|
|
107
|
+
throw new Error("Pairing credentials do not match this ceremony");
|
|
108
|
+
}
|
|
109
|
+
if (contents.expiresAt < (expected.now ?? Date.now())) {
|
|
110
|
+
throw new Error("Pairing credentials have expired");
|
|
111
|
+
}
|
|
112
|
+
return contents;
|
|
113
|
+
}
|
|
114
|
+
/** Creates the temporary destination identity's receipt after normal runtime initialization. */
|
|
115
|
+
function createSignedPairingInstallationReceipt(temporaryDestinationDevice, transcriptHash, initializedDevice, installedAt = Date.now()) {
|
|
116
|
+
const contents = types_1.pairingInstallationReceiptContentsSchema.parse({
|
|
117
|
+
version: constants_1.DEVICE_PAIRING_PROTOCOL_VERSION,
|
|
118
|
+
transcriptHash,
|
|
119
|
+
initializedDevice,
|
|
120
|
+
installedAt,
|
|
121
|
+
});
|
|
122
|
+
return types_1.signedPairingInstallationReceiptSchema.parse(temporaryDestinationDevice.signObjectWithSecretKey(contents));
|
|
123
|
+
}
|
|
124
|
+
/** Verifies that an installation receipt came from the authenticated temporary destination. */
|
|
125
|
+
function verifySignedPairingInstallationReceipt(receipt, expected) {
|
|
126
|
+
const parsed = types_1.signedPairingInstallationReceiptSchema.parse(receipt);
|
|
127
|
+
if (parsed.publicKey !== expected.temporaryDestinationPublicKey) {
|
|
128
|
+
throw new Error("Pairing receipt came from an unexpected device");
|
|
129
|
+
}
|
|
130
|
+
const contents = types_1.pairingInstallationReceiptContentsSchema.parse((0, keys_1.openSignedObject)(parsed));
|
|
131
|
+
if (contents.transcriptHash !== expected.transcriptHash) {
|
|
132
|
+
throw new Error("Pairing receipt does not match this ceremony");
|
|
133
|
+
}
|
|
134
|
+
return contents.initializedDevice;
|
|
135
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|