@peers-app/peers-sdk 0.23.14 → 0.25.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.
Files changed (96) hide show
  1. package/dist/context/user-context.d.ts +8 -0
  2. package/dist/context/user-context.js +9 -1
  3. package/dist/contracts/index.d.ts +1 -0
  4. package/dist/contracts/index.js +4 -1
  5. package/dist/contracts/system/index.d.ts +2 -0
  6. package/dist/contracts/system/index.js +5 -1
  7. package/dist/contracts/system/invites-contract.d.ts +121 -0
  8. package/dist/contracts/system/invites-contract.js +150 -0
  9. package/dist/data/create-group.d.ts +24 -0
  10. package/dist/data/create-group.js +61 -0
  11. package/dist/data/group-secret.d.ts +10 -0
  12. package/dist/data/group-secret.js +26 -0
  13. package/dist/data/group-share.d.ts +0 -20
  14. package/dist/data/group-share.js +0 -35
  15. package/dist/data/index.d.ts +2 -0
  16. package/dist/data/index.js +2 -0
  17. package/dist/data/invites.d.ts +350 -0
  18. package/dist/data/invites.js +99 -0
  19. package/dist/data/orm/data-query.d.ts +1 -0
  20. package/dist/data/orm/sql.data-source.d.ts +3 -2
  21. package/dist/data/orm/sql.data-source.js +5 -3
  22. package/dist/data/orm/subscribable.data-source.d.ts +3 -2
  23. package/dist/data/orm/subscribable.data-source.js +7 -11
  24. package/dist/data/users.d.ts +11 -0
  25. package/dist/data/users.js +17 -0
  26. package/dist/device/device-election.d.ts +23 -3
  27. package/dist/device/device-election.js +15 -0
  28. package/dist/device/device-election.test.js +73 -0
  29. package/dist/device-pairing/constants.d.ts +27 -9
  30. package/dist/device-pairing/constants.js +28 -10
  31. package/dist/device-pairing/cpace.d.ts +46 -0
  32. package/dist/device-pairing/cpace.js +173 -0
  33. package/dist/device-pairing/cpace.test.js +116 -0
  34. package/dist/device-pairing/crypto.d.ts +40 -15
  35. package/dist/device-pairing/crypto.js +74 -33
  36. package/dist/device-pairing/device-pairing.test.js +156 -119
  37. package/dist/device-pairing/index.d.ts +3 -1
  38. package/dist/device-pairing/index.js +3 -1
  39. package/dist/device-pairing/pairing-code.d.ts +27 -0
  40. package/dist/device-pairing/pairing-code.js +84 -0
  41. package/dist/device-pairing/pgp-word-list.d.ts +14 -0
  42. package/dist/device-pairing/pgp-word-list.js +531 -0
  43. package/dist/device-pairing/signaling.d.ts +17 -6
  44. package/dist/device-pairing/signaling.js +31 -8
  45. package/dist/device-pairing/transcript.d.ts +3 -3
  46. package/dist/device-pairing/transcript.js +6 -13
  47. package/dist/device-pairing/types.d.ts +870 -224
  48. package/dist/device-pairing/types.js +186 -108
  49. package/dist/index.d.ts +1 -2
  50. package/dist/index.js +1 -2
  51. package/dist/invites/data-box-schema.d.ts +15 -0
  52. package/dist/invites/data-box-schema.js +15 -0
  53. package/dist/invites/group-approval.d.ts +43 -0
  54. package/dist/invites/group-approval.js +155 -0
  55. package/dist/invites/group-approval.test.js +221 -0
  56. package/dist/invites/index.d.ts +5 -0
  57. package/dist/{group-invite → invites}/index.js +5 -3
  58. package/dist/invites/invite-epoch.d.ts +18 -0
  59. package/dist/invites/invite-epoch.js +35 -0
  60. package/dist/invites/invite-messages.d.ts +1354 -0
  61. package/dist/invites/invite-messages.js +145 -0
  62. package/dist/invites/invite-token.d.ts +334 -0
  63. package/dist/invites/invite-token.js +178 -0
  64. package/dist/invites/invite-token.test.d.ts +1 -0
  65. package/dist/invites/invite-token.test.js +123 -0
  66. package/dist/keys.d.ts +17 -1
  67. package/dist/keys.js +58 -5
  68. package/dist/keys.test.js +113 -0
  69. package/dist/rpc-types.d.ts +5 -4
  70. package/dist/rpc-types.js +2 -1
  71. package/dist/serial-json.d.ts +4 -1
  72. package/dist/serial-json.js +4 -1
  73. package/dist/system-ids.d.ts +22 -0
  74. package/dist/system-ids.js +23 -1
  75. package/package.json +1 -1
  76. package/dist/device-pairing/invitation.d.ts +0 -13
  77. package/dist/device-pairing/invitation.js +0 -85
  78. package/dist/group-invite/group-invite.d.ts +0 -53
  79. package/dist/group-invite/group-invite.js +0 -231
  80. package/dist/group-invite/group-invite.pvars.d.ts +0 -94
  81. package/dist/group-invite/group-invite.pvars.js +0 -139
  82. package/dist/group-invite/group-invite.types.d.ts +0 -124
  83. package/dist/group-invite/group-invite.types.js +0 -8
  84. package/dist/group-invite/index.d.ts +0 -3
  85. package/dist/user-connect/connection-code.d.ts +0 -67
  86. package/dist/user-connect/connection-code.js +0 -176
  87. package/dist/user-connect/connection-code.test.js +0 -213
  88. package/dist/user-connect/index.d.ts +0 -3
  89. package/dist/user-connect/index.js +0 -19
  90. package/dist/user-connect/user-connect.pvars.d.ts +0 -20
  91. package/dist/user-connect/user-connect.pvars.js +0 -63
  92. package/dist/user-connect/user-connect.pvars.test.js +0 -46
  93. package/dist/user-connect/user-connect.types.d.ts +0 -94
  94. package/dist/user-connect/user-connect.types.js +0 -68
  95. /package/dist/{user-connect/connection-code.test.d.ts → device-pairing/cpace.test.d.ts} +0 -0
  96. /package/dist/{user-connect/user-connect.pvars.test.d.ts → invites/group-approval.test.d.ts} +0 -0
@@ -308,6 +308,79 @@ describe("device-election", () => {
308
308
  expect(result).toBeDefined();
309
309
  expect([device1, device2, device3]).toContain(result?.deviceId);
310
310
  });
311
+ describe("alternativeRoutes", () => {
312
+ it("never sheds a device whose only known route is through us when others have alternatives", () => {
313
+ const stranded = (0, utils_1.newid)();
314
+ const others = [(0, utils_1.newid)(), (0, utils_1.newid)(), (0, utils_1.newid)()];
315
+ const connections = [
316
+ // worst latency and most groups would normally make this the victim
317
+ { ...createConnection(stranded, 900, 0.5), groups: ["g"], alternativeRoutes: 0 },
318
+ ...others.map((id) => ({
319
+ ...createConnection(id, 5, 0.001),
320
+ groups: ["g", "h"],
321
+ alternativeRoutes: 3,
322
+ })),
323
+ ];
324
+ for (let i = 0; i < 20; i++) {
325
+ const result = (0, device_election_1.getLeastPreferredConnection)({
326
+ connections: (0, lodash_1.shuffle)(connections),
327
+ preferredDeviceIds: [],
328
+ preferredByDeviceIds: [],
329
+ });
330
+ expect(result).toBeDefined();
331
+ expect(result?.deviceId).not.toBe(stranded);
332
+ expect(others).toContain(result?.deviceId);
333
+ }
334
+ });
335
+ it("falls back to the usual ordering when no candidate is known to have alternatives", () => {
336
+ const device1 = (0, utils_1.newid)();
337
+ const device2 = (0, utils_1.newid)();
338
+ const connections = [
339
+ { ...createConnection(device1, 10, 0.001), groups: ["g"], alternativeRoutes: 0 },
340
+ { ...createConnection(device2, 500, 0.5), groups: ["g"], alternativeRoutes: 0 },
341
+ ];
342
+ const result = (0, device_election_1.getLeastPreferredConnection)({
343
+ connections,
344
+ preferredDeviceIds: [],
345
+ preferredByDeviceIds: [],
346
+ });
347
+ expect(result?.deviceId).toBe(device2);
348
+ });
349
+ it("treats unknown topology as neither protected nor preferred", () => {
350
+ const unknown = (0, utils_1.newid)();
351
+ const stranded = (0, utils_1.newid)();
352
+ const connected = (0, utils_1.newid)();
353
+ const connections = [
354
+ { ...createConnection(unknown, 500, 0.5), groups: ["g"] },
355
+ { ...createConnection(stranded, 10, 0.001), groups: ["g"], alternativeRoutes: 0 },
356
+ { ...createConnection(connected, 10, 0.001), groups: ["g"], alternativeRoutes: 2 },
357
+ ];
358
+ for (let i = 0; i < 20; i++) {
359
+ const result = (0, device_election_1.getLeastPreferredConnection)({
360
+ connections: (0, lodash_1.shuffle)(connections),
361
+ preferredDeviceIds: [],
362
+ preferredByDeviceIds: [],
363
+ });
364
+ // the stranded device is excluded; unknown stays in the pool and loses on latency
365
+ expect(result?.deviceId).toBe(unknown);
366
+ }
367
+ });
368
+ it("still respects preference tiers before route protection", () => {
369
+ const stranded = (0, utils_1.newid)();
370
+ const preferred = (0, utils_1.newid)();
371
+ const connections = [
372
+ { ...createConnection(stranded), groups: ["g"], alternativeRoutes: 0 },
373
+ { ...createConnection(preferred), groups: ["g"], alternativeRoutes: 4 },
374
+ ];
375
+ // the only non-preferred candidate is the stranded one, so the tier leaves just it
376
+ const result = (0, device_election_1.getLeastPreferredConnection)({
377
+ connections,
378
+ preferredDeviceIds: [preferred],
379
+ preferredByDeviceIds: [],
380
+ });
381
+ expect(result?.deviceId).toBe(stranded);
382
+ });
383
+ });
311
384
  });
312
385
  describe("Network simulations", () => {
313
386
  it.skip("should work as connections grow", () => {
@@ -1,7 +1,15 @@
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. */
1
+ /**
2
+ * Wire-protocol version for the temporary device-pairing ceremony.
3
+ *
4
+ * - v2: reversed roles + PAKE.
5
+ * - v3: negotiated data transport (WebRTC or direct WebSocket); endpoint nonces move from the
6
+ * SDP descriptions into the transport messages and the transcript binds `transportBinding`.
7
+ */
8
+ export declare const DEVICE_PAIRING_PROTOCOL_VERSION: 3;
9
+ /** Maximum lifetime of a pairing code and its signaling room. */
4
10
  export declare const DEVICE_PAIRING_TTL_MS: number;
11
+ /** Largest nameplate the service allocates; codes read `<1..9999>-<word>-<word>-<word>`. */
12
+ export declare const DEVICE_PAIRING_MAX_NAMEPLATE = 9999;
5
13
  /** Socket.IO namespace reserved for device-pairing signaling. */
6
14
  export declare const DEVICE_PAIRING_SIGNALING_NAMESPACE = "/device-pairing-v1";
7
15
  /** Dedicated Socket.IO engine path with a pairing-sized transport buffer. */
@@ -22,13 +30,23 @@ export declare const DEVICE_PAIRING_MAX_SIGNAL_BYTES: number;
22
30
  export declare const DEVICE_PAIRING_MAX_TRANSPORT_BYTES: number;
23
31
  /** Maximum ICE candidates accepted from either endpoint. */
24
32
  export declare const DEVICE_PAIRING_MAX_ICE_CANDIDATES_PER_ROLE = 64;
33
+ /** Maximum direct WebSocket URLs a destination may advertise in its transport options. */
34
+ export declare const DEVICE_PAIRING_MAX_WEBSOCKET_URLS = 8;
35
+ /**
36
+ * Socket.IO namespace a destination hosts for the direct WebSocket pairing transport. The
37
+ * source authenticates with the PAKE-derived WebSocket token in `auth.token`.
38
+ */
39
+ export declare const DEVICE_PAIRING_WEBSOCKET_NAMESPACE = "/device-pairing-transport-v1";
25
40
  /** Maximum tolerated source/destination wall-clock difference. */
26
41
  export declare const DEVICE_PAIRING_CLOCK_SKEW_MS: number;
27
42
  /** Event name used to publish renderer-safe pairing state. */
28
43
  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";
44
+ /** Domain-separation label for the PAKE-derived signaling authentication key. */
45
+ export declare const DEVICE_PAIRING_SIGNALING_LABEL = "peers-pairing-signaling-v3";
46
+ /** Domain-separation label for the PAKE-derived transcript confirmation key. */
47
+ export declare const DEVICE_PAIRING_CONFIRM_LABEL = "peers-pairing-confirm-v3";
48
+ /**
49
+ * Domain-separation label for the token a source presents when it connects to a destination's
50
+ * direct WebSocket pairing listener. Derived from the signaling key; never sent via the relay.
51
+ */
52
+ export declare const DEVICE_PAIRING_WEBSOCKET_TOKEN_LABEL = "peers-pairing-websocket-token-v3";
@@ -1,10 +1,18 @@
1
1
  "use strict";
2
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. */
3
+ exports.DEVICE_PAIRING_WEBSOCKET_TOKEN_LABEL = exports.DEVICE_PAIRING_CONFIRM_LABEL = exports.DEVICE_PAIRING_SIGNALING_LABEL = exports.DEVICE_PAIRING_STATE_EVENT = exports.DEVICE_PAIRING_CLOCK_SKEW_MS = exports.DEVICE_PAIRING_WEBSOCKET_NAMESPACE = exports.DEVICE_PAIRING_MAX_WEBSOCKET_URLS = 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_MAX_NAMEPLATE = exports.DEVICE_PAIRING_TTL_MS = exports.DEVICE_PAIRING_PROTOCOL_VERSION = void 0;
4
+ /**
5
+ * Wire-protocol version for the temporary device-pairing ceremony.
6
+ *
7
+ * - v2: reversed roles + PAKE.
8
+ * - v3: negotiated data transport (WebRTC or direct WebSocket); endpoint nonces move from the
9
+ * SDP descriptions into the transport messages and the transcript binds `transportBinding`.
10
+ */
11
+ exports.DEVICE_PAIRING_PROTOCOL_VERSION = 3;
12
+ /** Maximum lifetime of a pairing code and its signaling room. */
7
13
  exports.DEVICE_PAIRING_TTL_MS = 5 * 60 * 1000;
14
+ /** Largest nameplate the service allocates; codes read `<1..9999>-<word>-<word>-<word>`. */
15
+ exports.DEVICE_PAIRING_MAX_NAMEPLATE = 9999;
8
16
  /** Socket.IO namespace reserved for device-pairing signaling. */
9
17
  exports.DEVICE_PAIRING_SIGNALING_NAMESPACE = "/device-pairing-v1";
10
18
  /** Dedicated Socket.IO engine path with a pairing-sized transport buffer. */
@@ -25,13 +33,23 @@ exports.DEVICE_PAIRING_MAX_SIGNAL_BYTES = 72 * 1024;
25
33
  exports.DEVICE_PAIRING_MAX_TRANSPORT_BYTES = 96 * 1024;
26
34
  /** Maximum ICE candidates accepted from either endpoint. */
27
35
  exports.DEVICE_PAIRING_MAX_ICE_CANDIDATES_PER_ROLE = 64;
36
+ /** Maximum direct WebSocket URLs a destination may advertise in its transport options. */
37
+ exports.DEVICE_PAIRING_MAX_WEBSOCKET_URLS = 8;
38
+ /**
39
+ * Socket.IO namespace a destination hosts for the direct WebSocket pairing transport. The
40
+ * source authenticates with the PAKE-derived WebSocket token in `auth.token`.
41
+ */
42
+ exports.DEVICE_PAIRING_WEBSOCKET_NAMESPACE = "/device-pairing-transport-v1";
28
43
  /** Maximum tolerated source/destination wall-clock difference. */
29
44
  exports.DEVICE_PAIRING_CLOCK_SKEW_MS = 2 * 60 * 1000;
30
45
  /** Event name used to publish renderer-safe pairing state. */
31
46
  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";
47
+ /** Domain-separation label for the PAKE-derived signaling authentication key. */
48
+ exports.DEVICE_PAIRING_SIGNALING_LABEL = "peers-pairing-signaling-v3";
49
+ /** Domain-separation label for the PAKE-derived transcript confirmation key. */
50
+ exports.DEVICE_PAIRING_CONFIRM_LABEL = "peers-pairing-confirm-v3";
51
+ /**
52
+ * Domain-separation label for the token a source presents when it connects to a destination's
53
+ * direct WebSocket pairing listener. Derived from the signaling key; never sent via the relay.
54
+ */
55
+ exports.DEVICE_PAIRING_WEBSOCKET_TOKEN_LABEL = "peers-pairing-websocket-token-v3";
@@ -0,0 +1,46 @@
1
+ /** Role in the one-round CPace exchange; the initiator's share is listed first in the transcript. */
2
+ export type CpaceRole = "initiator" | "responder";
3
+ /** Inputs that both parties must agree on for the exchange to succeed. */
4
+ export interface CpaceParameters {
5
+ /** Password-related string (PRS); for pairing, the code words. */
6
+ password: Uint8Array;
7
+ /** Channel identifier (CI) binding the exchange to one rendezvous. */
8
+ channelId: Uint8Array;
9
+ /** Optional session id (sid); empty when the channel identifier is already unique. */
10
+ sessionId?: Uint8Array;
11
+ /** Associated data sent alongside the initiator's share. */
12
+ initiatorAd?: Uint8Array;
13
+ /** Associated data sent alongside the responder's share. */
14
+ responderAd?: Uint8Array;
15
+ }
16
+ /** One party's in-progress exchange. */
17
+ export interface CpaceSession {
18
+ /** Public share (32-byte X25519 u-coordinate) to send to the other party. */
19
+ readonly share: Uint8Array;
20
+ /**
21
+ * Consumes the remote share and returns the 64-byte ISK.
22
+ * @throws when the remote share encodes a low-order point (the exchange must be aborted).
23
+ */
24
+ finish(remoteShare: Uint8Array): Uint8Array;
25
+ }
26
+ /** LEB128 length prefix as defined by the draft's `prepend_len`. */
27
+ export declare function cpacePrependLength(data: Uint8Array): Uint8Array;
28
+ /** Length-prefixed concatenation (`lv_cat`). */
29
+ export declare function cpaceLvCat(...parts: Uint8Array[]): Uint8Array;
30
+ /** Builds the `generator_string` that is hashed into the secret generator. */
31
+ export declare function cpaceGeneratorString(password: Uint8Array, channelId: Uint8Array, sessionId: Uint8Array): Uint8Array;
32
+ /** Elligator 2 map from a field element to a Curve25519 u-coordinate (RFC 9380 `map_to_curve_elligator2`). */
33
+ export declare function cpaceElligator2(u: bigint): Uint8Array;
34
+ /** Derives the password-dependent generator `g` for one exchange. */
35
+ export declare function cpaceCalculateGenerator(password: Uint8Array, channelId: Uint8Array, sessionId?: Uint8Array): Uint8Array;
36
+ /** X25519 scalar multiplication that rejects results in the small subgroup (`scalar_mult_vfy`). */
37
+ export declare function cpaceScalarMultVerify(scalar: Uint8Array, point: Uint8Array): Uint8Array;
38
+ /** Initiator/responder transcript (`transcript_ir`). */
39
+ export declare function cpaceTranscript(initiatorShare: Uint8Array, initiatorAd: Uint8Array, responderShare: Uint8Array, responderAd: Uint8Array): Uint8Array;
40
+ /** Final ISK derivation: `H(lv_cat(DSI_ISK, sid, K) || transcript)`. */
41
+ export declare function cpaceDeriveIsk(sharedPoint: Uint8Array, sessionId: Uint8Array, transcript: Uint8Array): Uint8Array;
42
+ /**
43
+ * Starts one side of a CPace exchange. The returned share is safe to send in the clear.
44
+ * `scalar` may be injected for deterministic test vectors; production callers omit it.
45
+ */
46
+ export declare function cpaceStart(role: CpaceRole, parameters: CpaceParameters, scalar?: Uint8Array): CpaceSession;
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cpacePrependLength = cpacePrependLength;
4
+ exports.cpaceLvCat = cpaceLvCat;
5
+ exports.cpaceGeneratorString = cpaceGeneratorString;
6
+ exports.cpaceElligator2 = cpaceElligator2;
7
+ exports.cpaceCalculateGenerator = cpaceCalculateGenerator;
8
+ exports.cpaceScalarMultVerify = cpaceScalarMultVerify;
9
+ exports.cpaceTranscript = cpaceTranscript;
10
+ exports.cpaceDeriveIsk = cpaceDeriveIsk;
11
+ exports.cpaceStart = cpaceStart;
12
+ const sha2_1 = require("@noble/hashes/sha2");
13
+ const nacl = require("tweetnacl");
14
+ /**
15
+ * CPace balanced PAKE over X25519 with SHA-512 (cipher suite `CPace255`), implemented per
16
+ * draft-irtf-cfrg-cpace. Both parties derive the same 64-byte intermediate session key (ISK)
17
+ * if and only if they used the same password, channel identifier, and session id, and saw
18
+ * each other's public shares unmodified. A wrong password yields an unrelated ISK; nothing
19
+ * about the password can be recovered from the transcript.
20
+ *
21
+ * Group operations use `tweetnacl`'s X25519 (`scalarMult`); the Elligator 2 map that turns
22
+ * the hashed password into a secret generator is implemented here on `bigint` field
23
+ * arithmetic (Curve25519, `A = 486662`, `Z = 2`). Verified against the draft's test vectors.
24
+ */
25
+ const FIELD_PRIME = 2n ** 255n - 19n;
26
+ const CURVE_A = 486662n;
27
+ const ELLIGATOR_Z = 2n;
28
+ const FIELD_SIZE_BYTES = 32;
29
+ const HASH_BLOCK_BYTES = 128;
30
+ const DSI = new TextEncoder().encode("CPace255");
31
+ const DSI_ISK = new TextEncoder().encode("CPace255_ISK");
32
+ function mod(value) {
33
+ const result = value % FIELD_PRIME;
34
+ return result >= 0n ? result : result + FIELD_PRIME;
35
+ }
36
+ function modPow(base, exponent) {
37
+ let result = 1n;
38
+ let current = mod(base);
39
+ let remaining = exponent;
40
+ while (remaining > 0n) {
41
+ if (remaining & 1n) {
42
+ result = mod(result * current);
43
+ }
44
+ current = mod(current * current);
45
+ remaining >>= 1n;
46
+ }
47
+ return result;
48
+ }
49
+ function modInverse(value) {
50
+ return modPow(value, FIELD_PRIME - 2n);
51
+ }
52
+ /** LEB128 length prefix as defined by the draft's `prepend_len`. */
53
+ function cpacePrependLength(data) {
54
+ const prefix = [];
55
+ let length = data.length;
56
+ while (true) {
57
+ if (length < 128) {
58
+ prefix.push(length);
59
+ }
60
+ else {
61
+ prefix.push((length & 0x7f) + 0x80);
62
+ }
63
+ length >>= 7;
64
+ if (length === 0) {
65
+ break;
66
+ }
67
+ }
68
+ return concatBytes(Uint8Array.from(prefix), data);
69
+ }
70
+ /** Length-prefixed concatenation (`lv_cat`). */
71
+ function cpaceLvCat(...parts) {
72
+ return concatBytes(...parts.map((part) => cpacePrependLength(part)));
73
+ }
74
+ function concatBytes(...parts) {
75
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
76
+ const result = new Uint8Array(total);
77
+ let offset = 0;
78
+ for (const part of parts) {
79
+ result.set(part, offset);
80
+ offset += part.length;
81
+ }
82
+ return result;
83
+ }
84
+ /** Builds the `generator_string` that is hashed into the secret generator. */
85
+ function cpaceGeneratorString(password, channelId, sessionId) {
86
+ const zeroPadLength = Math.max(0, HASH_BLOCK_BYTES - 1 - cpacePrependLength(password).length - cpacePrependLength(DSI).length);
87
+ return cpaceLvCat(DSI, password, new Uint8Array(zeroPadLength), channelId, sessionId);
88
+ }
89
+ function decodeUCoordinate(bytes) {
90
+ const copy = Uint8Array.from(bytes);
91
+ copy[FIELD_SIZE_BYTES - 1] &= 0x7f;
92
+ let value = 0n;
93
+ for (let index = copy.length - 1; index >= 0; index--) {
94
+ value = (value << 8n) | BigInt(copy[index]);
95
+ }
96
+ return value;
97
+ }
98
+ function encodeUCoordinate(value) {
99
+ const bytes = new Uint8Array(FIELD_SIZE_BYTES);
100
+ let remaining = mod(value);
101
+ for (let index = 0; index < FIELD_SIZE_BYTES; index++) {
102
+ bytes[index] = Number(remaining & 0xffn);
103
+ remaining >>= 8n;
104
+ }
105
+ return bytes;
106
+ }
107
+ /** Elligator 2 map from a field element to a Curve25519 u-coordinate (RFC 9380 `map_to_curve_elligator2`). */
108
+ function cpaceElligator2(u) {
109
+ const v = mod(-CURVE_A * modInverse(1n + ELLIGATOR_Z * u * u));
110
+ const legendre = modPow(mod(v * v * v + CURVE_A * v * v + v), (FIELD_PRIME - 1n) / 2n);
111
+ const x = mod(legendre * v - (1n - legendre) * CURVE_A * modInverse(2n));
112
+ return encodeUCoordinate(x);
113
+ }
114
+ /** Derives the password-dependent generator `g` for one exchange. */
115
+ function cpaceCalculateGenerator(password, channelId, sessionId = new Uint8Array(0)) {
116
+ const digest = (0, sha2_1.sha512)(cpaceGeneratorString(password, channelId, sessionId));
117
+ return cpaceElligator2(decodeUCoordinate(digest.slice(0, FIELD_SIZE_BYTES)));
118
+ }
119
+ function isAllZero(bytes) {
120
+ let accumulator = 0;
121
+ for (const byte of bytes) {
122
+ accumulator |= byte;
123
+ }
124
+ return accumulator === 0;
125
+ }
126
+ /** X25519 scalar multiplication that rejects results in the small subgroup (`scalar_mult_vfy`). */
127
+ function cpaceScalarMultVerify(scalar, point) {
128
+ if (scalar.length !== FIELD_SIZE_BYTES || point.length !== FIELD_SIZE_BYTES) {
129
+ throw new Error("CPace shares must be 32 bytes");
130
+ }
131
+ const result = nacl.scalarMult(scalar, point);
132
+ if (isAllZero(result)) {
133
+ throw new Error("CPace received a low-order point");
134
+ }
135
+ return result;
136
+ }
137
+ /** Initiator/responder transcript (`transcript_ir`). */
138
+ function cpaceTranscript(initiatorShare, initiatorAd, responderShare, responderAd) {
139
+ return concatBytes(cpaceLvCat(initiatorShare, initiatorAd), cpaceLvCat(responderShare, responderAd));
140
+ }
141
+ /** Final ISK derivation: `H(lv_cat(DSI_ISK, sid, K) || transcript)`. */
142
+ function cpaceDeriveIsk(sharedPoint, sessionId, transcript) {
143
+ return (0, sha2_1.sha512)(concatBytes(cpaceLvCat(DSI_ISK, sessionId, sharedPoint), transcript));
144
+ }
145
+ /**
146
+ * Starts one side of a CPace exchange. The returned share is safe to send in the clear.
147
+ * `scalar` may be injected for deterministic test vectors; production callers omit it.
148
+ */
149
+ function cpaceStart(role, parameters, scalar = nacl.randomBytes(FIELD_SIZE_BYTES)) {
150
+ const sessionId = parameters.sessionId ?? new Uint8Array(0);
151
+ const initiatorAd = parameters.initiatorAd ?? new Uint8Array(0);
152
+ const responderAd = parameters.responderAd ?? new Uint8Array(0);
153
+ const generator = cpaceCalculateGenerator(parameters.password, parameters.channelId, sessionId);
154
+ const share = cpaceScalarMultVerify(scalar, generator);
155
+ let finished = false;
156
+ return {
157
+ share,
158
+ finish(remoteShare) {
159
+ if (finished) {
160
+ throw new Error("CPace session already finished");
161
+ }
162
+ finished = true;
163
+ const sharedPoint = cpaceScalarMultVerify(scalar, remoteShare);
164
+ const transcript = role === "initiator"
165
+ ? cpaceTranscript(share, initiatorAd, remoteShare, responderAd)
166
+ : cpaceTranscript(remoteShare, initiatorAd, share, responderAd);
167
+ const isk = cpaceDeriveIsk(sharedPoint, sessionId, transcript);
168
+ scalar.fill(0);
169
+ sharedPoint.fill(0);
170
+ return isk;
171
+ },
172
+ };
173
+ }
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const cpace_1 = require("./cpace");
4
+ // Test vectors from draft-irtf-cfrg-cpace, "CPace using group X25519 and hash SHA-512".
5
+ const VECTOR = {
6
+ PRS: "50617373776F7264",
7
+ CI: "0B415F696E69746961746F720B425F726573706F6E646572",
8
+ sid: "7E4B4791D6A8EF019B936C79FB7F2C57",
9
+ g: "D04BF6D41F6A289632A2E929FA29BEBD51092512A7829FDDE7D314B62F05A73F",
10
+ ya: "21B4F4BD9E64ED355C3EB676A28EBEDAF6D8F17BDC365995B319097153044080",
11
+ ADa: "414461",
12
+ Ya: "1D13C89278CDADD826F6D8D7F887701430F8380DDC17611CDD6DC989CE0C9F32",
13
+ yb: "848B0779FF415F0AF4EA14DF9DD1D3C29AC41D836C7808896C4EBA19C51AC40A",
14
+ ADb: "414462",
15
+ Yb: "248CCCF6D5CDC3646F0AD593F9E6CEF4E69D4945F8372E623512ECEA32185623",
16
+ K: "5B067EFFBDC0B2A0E1D907B21EBB25CFEDB96A852179A847C37E43EE71322C6B",
17
+ ISK_IR: "6E19B875F7A561D6B3CA3DBB9EF42AC55DE3E717881018204B8922B4D5E53BB2AA82C300BEA7B65D2B671DA71922DDF6472301B79BC270ADFA8BF413285F2263",
18
+ };
19
+ // scalar_mult_vfy vectors: low-order (and non-canonical low-order) inputs abort; the rest
20
+ // must produce the draft's exact outputs, proving bit #255 is cleared before the ladder.
21
+ const VFY_SCALAR = "AF46E36BF0527C9D3B16154B82465EDD62144C0AC1FC5A18506A2244BA449AFF";
22
+ const LOW_ORDER_POINTS = [
23
+ "0000000000000000000000000000000000000000000000000000000000000000",
24
+ "0100000000000000000000000000000000000000000000000000000000000000",
25
+ "ECFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F",
26
+ "E0EB7A7C3B41B8AE1656E3FAF19FC46ADA098DEB9C32B1FD866205165F49B800",
27
+ "5F9C95BCA3508C24B1D0B1559C83EF5B04445CC4581C8E86D8224EDDD09F1157",
28
+ "EDFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F",
29
+ "EEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F",
30
+ ];
31
+ const NON_CANONICAL_POINTS = [
32
+ [
33
+ "DAFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
34
+ "D8E2C776BBACD510D09FD9278B7EDCD25FC5AE9ADFBA3B6E040E8D3B71B21806",
35
+ ],
36
+ [
37
+ "DBFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
38
+ "C85C655EBE8BE44BA9C0FFDE69F2FE10194458D137F09BBFF725CE58803CDB38",
39
+ ],
40
+ [
41
+ "D9FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
42
+ "DB64DAFA9B8FDD136914E61461935FE92AA372CB056314E1231BC4EC12417456",
43
+ ],
44
+ [
45
+ "CDEB7A7C3B41B8AE1656E3FAF19FC46ADA098DEB9C32B1FD866205165F49B880",
46
+ "E062DCD5376D58297BE2618C7498F55BAA07D7E03184E8AADA20BCA28888BF7A",
47
+ ],
48
+ [
49
+ "4C9C95BCA3508C24B1D0B1559C83EF5B04445CC4581C8E86D8224EDDD09F11D7",
50
+ "993C6AD11C4C29DA9A56F7691FD0FF8D732E49DE6250B6C2E80003FF4629A175",
51
+ ],
52
+ ];
53
+ function hex(value) {
54
+ return Uint8Array.from(Buffer.from(value, "hex"));
55
+ }
56
+ function toHex(value) {
57
+ return Buffer.from(value).toString("hex").toUpperCase();
58
+ }
59
+ describe("cpace string utilities", () => {
60
+ it("matches prepend_len and lv_cat vectors", () => {
61
+ expect(toHex((0, cpace_1.cpacePrependLength)(new Uint8Array(0)))).toBe("00");
62
+ expect(toHex((0, cpace_1.cpacePrependLength)(Buffer.from("1234")))).toBe("0431323334");
63
+ expect(toHex((0, cpace_1.cpacePrependLength)(new Uint8Array(128))).slice(0, 4)).toBe("8001");
64
+ expect(toHex((0, cpace_1.cpaceLvCat)(Buffer.from("1234"), Buffer.from("5"), new Uint8Array(0), Buffer.from("678")))).toBe("043132333401350003363738");
65
+ });
66
+ it("matches transcript_ir vector", () => {
67
+ expect(toHex((0, cpace_1.cpaceTranscript)(Buffer.from("123"), Buffer.from("PartyA"), Buffer.from("234"), Buffer.from("PartyB")))).toBe("03313233065061727479410332333406506172747942");
68
+ });
69
+ });
70
+ describe("cpace CPace255 vectors", () => {
71
+ it("derives the generator string and secret generator", () => {
72
+ const generatorString = (0, cpace_1.cpaceGeneratorString)(hex(VECTOR.PRS), hex(VECTOR.CI), hex(VECTOR.sid));
73
+ expect(generatorString).toHaveLength(170);
74
+ expect(toHex(generatorString).startsWith("0843506163653235350850617373776F72646D00")).toBe(true);
75
+ expect(toHex((0, cpace_1.cpaceCalculateGenerator)(hex(VECTOR.PRS), hex(VECTOR.CI), hex(VECTOR.sid)))).toBe(VECTOR.g);
76
+ });
77
+ it("derives both shares, the shared point, and the initiator/responder ISK", () => {
78
+ const parameters = {
79
+ password: hex(VECTOR.PRS),
80
+ channelId: hex(VECTOR.CI),
81
+ sessionId: hex(VECTOR.sid),
82
+ initiatorAd: hex(VECTOR.ADa),
83
+ responderAd: hex(VECTOR.ADb),
84
+ };
85
+ const initiator = (0, cpace_1.cpaceStart)("initiator", parameters, hex(VECTOR.ya));
86
+ const responder = (0, cpace_1.cpaceStart)("responder", parameters, hex(VECTOR.yb));
87
+ expect(toHex(initiator.share)).toBe(VECTOR.Ya);
88
+ expect(toHex(responder.share)).toBe(VECTOR.Yb);
89
+ expect(toHex((0, cpace_1.cpaceScalarMultVerify)(hex(VECTOR.ya), hex(VECTOR.Yb)))).toBe(VECTOR.K);
90
+ expect(toHex((0, cpace_1.cpaceDeriveIsk)(hex(VECTOR.K), hex(VECTOR.sid), (0, cpace_1.cpaceTranscript)(hex(VECTOR.Ya), hex(VECTOR.ADa), hex(VECTOR.Yb), hex(VECTOR.ADb))))).toBe(VECTOR.ISK_IR);
91
+ const initiatorIsk = toHex(initiator.finish(hex(VECTOR.Yb)));
92
+ const responderIsk = toHex(responder.finish(hex(VECTOR.Ya)));
93
+ expect(initiatorIsk).toBe(VECTOR.ISK_IR);
94
+ expect(responderIsk).toBe(VECTOR.ISK_IR);
95
+ });
96
+ it("rejects low-order points and clears bit #255 of non-canonical inputs", () => {
97
+ for (const point of LOW_ORDER_POINTS) {
98
+ expect(() => (0, cpace_1.cpaceScalarMultVerify)(hex(VFY_SCALAR), hex(point))).toThrow(/low-order/);
99
+ }
100
+ for (const [point, expected] of NON_CANONICAL_POINTS) {
101
+ expect(toHex((0, cpace_1.cpaceScalarMultVerify)(hex(VFY_SCALAR), hex(point)))).toBe(expected);
102
+ }
103
+ });
104
+ it("produces unrelated keys for different passwords and identical keys for the same password", () => {
105
+ const shared = { channelId: Buffer.from("channel"), sessionId: new Uint8Array(0) };
106
+ const good = (0, cpace_1.cpaceStart)("initiator", { ...shared, password: Buffer.from("guitar-revenge") });
107
+ const right = (0, cpace_1.cpaceStart)("responder", { ...shared, password: Buffer.from("guitar-revenge") });
108
+ const wrong = (0, cpace_1.cpaceStart)("responder", { ...shared, password: Buffer.from("guitar-reward") });
109
+ const rightShare = Uint8Array.from(right.share);
110
+ const wrongShare = Uint8Array.from(wrong.share);
111
+ const goodShare = Uint8Array.from(good.share);
112
+ expect(toHex(right.finish(goodShare))).toBe(toHex(good.finish(rightShare)));
113
+ const second = (0, cpace_1.cpaceStart)("initiator", { ...shared, password: Buffer.from("guitar-revenge") });
114
+ expect(toHex(wrong.finish(Uint8Array.from(second.share)))).not.toBe(toHex(second.finish(wrongShare)));
115
+ });
116
+ });
@@ -1,25 +1,50 @@
1
1
  import type { IDeviceInfo } from "../data/devices";
2
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;
3
+ import { type PairingCode } from "./pairing-code";
4
+ import { type PairingConfirmation, type PairingConfirmationContents, type PairingCredentialContents, type PairingRole, type SignedPairingInstallationReceipt, type UnsignedPairingSignal } from "./types";
5
+ /** Symmetric keys both endpoints derive from the PAKE intermediate session key. */
6
+ export interface PairingSessionKeys {
7
+ /** Authenticates transport, SDP, and ICE signaling relayed by the service. */
8
+ signalingKey: string;
9
+ /** Authenticates the post-handshake transcript confirmation. */
10
+ confirmKey: string;
11
+ }
12
+ /** One endpoint's in-progress PAKE over the pairing code. */
13
+ export interface PairingPake {
14
+ /** Base64url public share to send in the `pake` signal. */
15
+ share: string;
16
+ /** Consumes the remote share and derives the session keys; throws on an invalid share. */
17
+ finish(remoteShare: string): PairingSessionKeys;
18
+ }
19
+ /**
20
+ * Starts CPace for one role. The source is the initiator and the destination the responder;
21
+ * the exchange is bound to the service origin and nameplate so a relayed share from another
22
+ * room cannot be replayed.
23
+ */
24
+ export declare function startPairingPake(role: PairingRole, code: PairingCode, serviceOrigin: string): PairingPake;
25
+ /** Derives independent signaling and confirmation keys from a PAKE intermediate session key. */
26
+ export declare function derivePairingSessionKeys(isk: Uint8Array): PairingSessionKeys;
27
+ /**
28
+ * Derives the token a source presents (`auth.token`) when it connects to the destination's direct
29
+ * WebSocket pairing listener. Both endpoints compute it from the signaling key, so it never
30
+ * travels through the rendezvous; a socket without it is refused before any Connection exists.
31
+ */
32
+ export declare function derivePairingWebSocketToken(signalingKey: string): string;
10
33
  /** Computes the MAC over the canonical unsigned signaling envelope. */
11
34
  export declare function computePairingSignalMac(signal: UnsignedPairingSignal, signalingKey: string): string;
12
35
  /** Verifies a signaling MAC without short-circuiting on the first differing byte. */
13
36
  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;
37
+ /** Builds the local `pairing.confirm` message authenticated with the PAKE confirmation key. */
38
+ export declare function createPairingConfirmation(contents: Omit<PairingConfirmationContents, "version">, confirmKey: string): PairingConfirmation;
39
+ /**
40
+ * Verifies the remote confirmation's MAC, role, transcript, and identity binding.
41
+ * A MAC failure means the other endpoint used a different code or saw a different transcript.
42
+ */
43
+ export declare function verifyPairingConfirmation(value: unknown, confirmKey: string, expected: {
20
44
  role: PairingRole;
21
- now?: number;
22
- }): string;
45
+ transcriptHash: string;
46
+ device: IDeviceInfo;
47
+ }): PairingConfirmation;
23
48
  /** Signs and encrypts account credentials directly to the temporary destination identity. */
24
49
  export declare function createPairingCredentialEnvelope(sourceDevice: Device, destinationDevice: IDeviceInfo, credentials: {
25
50
  userId: string;