@peers-app/peers-sdk 0.23.13 → 0.24.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 (82) hide show
  1. package/dist/contracts/index.d.ts +1 -0
  2. package/dist/contracts/index.js +4 -1
  3. package/dist/contracts/system/index.d.ts +2 -0
  4. package/dist/contracts/system/index.js +5 -1
  5. package/dist/contracts/system/invites-contract.d.ts +121 -0
  6. package/dist/contracts/system/invites-contract.js +150 -0
  7. package/dist/data/create-group.d.ts +24 -0
  8. package/dist/data/create-group.js +61 -0
  9. package/dist/data/group-secret.d.ts +10 -0
  10. package/dist/data/group-secret.js +26 -0
  11. package/dist/data/group-share.d.ts +0 -20
  12. package/dist/data/group-share.js +0 -35
  13. package/dist/data/index.d.ts +2 -0
  14. package/dist/data/index.js +2 -0
  15. package/dist/data/invites.d.ts +350 -0
  16. package/dist/data/invites.js +99 -0
  17. package/dist/data/users.js +4 -0
  18. package/dist/device-pairing/constants.d.ts +27 -9
  19. package/dist/device-pairing/constants.js +28 -10
  20. package/dist/device-pairing/cpace.d.ts +46 -0
  21. package/dist/device-pairing/cpace.js +173 -0
  22. package/dist/device-pairing/cpace.test.js +116 -0
  23. package/dist/device-pairing/crypto.d.ts +40 -15
  24. package/dist/device-pairing/crypto.js +74 -33
  25. package/dist/device-pairing/device-pairing.test.js +156 -119
  26. package/dist/device-pairing/index.d.ts +3 -1
  27. package/dist/device-pairing/index.js +3 -1
  28. package/dist/device-pairing/pairing-code.d.ts +27 -0
  29. package/dist/device-pairing/pairing-code.js +84 -0
  30. package/dist/device-pairing/pgp-word-list.d.ts +14 -0
  31. package/dist/device-pairing/pgp-word-list.js +531 -0
  32. package/dist/device-pairing/signaling.d.ts +17 -6
  33. package/dist/device-pairing/signaling.js +31 -8
  34. package/dist/device-pairing/transcript.d.ts +3 -3
  35. package/dist/device-pairing/transcript.js +6 -13
  36. package/dist/device-pairing/types.d.ts +870 -224
  37. package/dist/device-pairing/types.js +186 -108
  38. package/dist/index.d.ts +1 -2
  39. package/dist/index.js +1 -2
  40. package/dist/invites/data-box-schema.d.ts +15 -0
  41. package/dist/invites/data-box-schema.js +15 -0
  42. package/dist/invites/group-approval.d.ts +43 -0
  43. package/dist/invites/group-approval.js +155 -0
  44. package/dist/invites/group-approval.test.js +221 -0
  45. package/dist/invites/index.d.ts +5 -0
  46. package/dist/{group-invite → invites}/index.js +5 -3
  47. package/dist/invites/invite-epoch.d.ts +18 -0
  48. package/dist/invites/invite-epoch.js +35 -0
  49. package/dist/invites/invite-messages.d.ts +1354 -0
  50. package/dist/invites/invite-messages.js +145 -0
  51. package/dist/invites/invite-token.d.ts +334 -0
  52. package/dist/invites/invite-token.js +178 -0
  53. package/dist/invites/invite-token.test.d.ts +1 -0
  54. package/dist/invites/invite-token.test.js +123 -0
  55. package/dist/rpc-types.d.ts +5 -4
  56. package/dist/rpc-types.js +2 -1
  57. package/dist/system-ids.d.ts +22 -0
  58. package/dist/system-ids.js +23 -1
  59. package/dist/types/peer-device.d.ts +15 -1
  60. package/dist/types/peer-device.js +15 -1
  61. package/package.json +1 -1
  62. package/dist/device-pairing/invitation.d.ts +0 -13
  63. package/dist/device-pairing/invitation.js +0 -85
  64. package/dist/group-invite/group-invite.d.ts +0 -53
  65. package/dist/group-invite/group-invite.js +0 -231
  66. package/dist/group-invite/group-invite.pvars.d.ts +0 -94
  67. package/dist/group-invite/group-invite.pvars.js +0 -139
  68. package/dist/group-invite/group-invite.types.d.ts +0 -124
  69. package/dist/group-invite/group-invite.types.js +0 -8
  70. package/dist/group-invite/index.d.ts +0 -3
  71. package/dist/user-connect/connection-code.d.ts +0 -67
  72. package/dist/user-connect/connection-code.js +0 -176
  73. package/dist/user-connect/connection-code.test.js +0 -213
  74. package/dist/user-connect/index.d.ts +0 -3
  75. package/dist/user-connect/index.js +0 -19
  76. package/dist/user-connect/user-connect.pvars.d.ts +0 -20
  77. package/dist/user-connect/user-connect.pvars.js +0 -63
  78. package/dist/user-connect/user-connect.pvars.test.js +0 -46
  79. package/dist/user-connect/user-connect.types.d.ts +0 -94
  80. package/dist/user-connect/user-connect.types.js +0 -68
  81. /package/dist/{user-connect/connection-code.test.d.ts → device-pairing/cpace.test.d.ts} +0 -0
  82. /package/dist/{user-connect/user-connect.pvars.test.d.ts → invites/group-approval.test.d.ts} +0 -0
@@ -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;
@@ -1,12 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.derivePairingRoomAccessToken = derivePairingRoomAccessToken;
4
- exports.hashPairingRoomAccessToken = hashPairingRoomAccessToken;
5
- exports.derivePairingSignalingKey = derivePairingSignalingKey;
3
+ exports.startPairingPake = startPairingPake;
4
+ exports.derivePairingSessionKeys = derivePairingSessionKeys;
5
+ exports.derivePairingWebSocketToken = derivePairingWebSocketToken;
6
6
  exports.computePairingSignalMac = computePairingSignalMac;
7
7
  exports.isPairingSignalMacValid = isPairingSignalMacValid;
8
- exports.createSignedPairingApproval = createSignedPairingApproval;
9
- exports.verifySignedPairingApproval = verifySignedPairingApproval;
8
+ exports.createPairingConfirmation = createPairingConfirmation;
9
+ exports.verifyPairingConfirmation = verifyPairingConfirmation;
10
10
  exports.createPairingCredentialEnvelope = createPairingCredentialEnvelope;
11
11
  exports.openPairingCredentialEnvelope = openPairingCredentialEnvelope;
12
12
  exports.createSignedPairingInstallationReceipt = createSignedPairingInstallationReceipt;
@@ -15,6 +15,8 @@ const hmac_1 = require("@noble/hashes/hmac");
15
15
  const sha2_1 = require("@noble/hashes/sha2");
16
16
  const keys_1 = require("../keys");
17
17
  const constants_1 = require("./constants");
18
+ const cpace_1 = require("./cpace");
19
+ const pairing_code_1 = require("./pairing-code");
18
20
  const types_1 = require("./types");
19
21
  const textEncoder = new TextEncoder();
20
22
  function computeHmac(key, value) {
@@ -30,17 +32,42 @@ function equalBytes(left, right) {
30
32
  }
31
33
  return difference === 0;
32
34
  }
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));
35
+ /**
36
+ * Starts CPace for one role. The source is the initiator and the destination the responder;
37
+ * the exchange is bound to the service origin and nameplate so a relayed share from another
38
+ * room cannot be replayed.
39
+ */
40
+ function startPairingPake(role, code, serviceOrigin) {
41
+ const session = (0, cpace_1.cpaceStart)(role === "source" ? "initiator" : "responder", {
42
+ password: (0, pairing_code_1.pairingPasswordFromCode)(code),
43
+ channelId: (0, pairing_code_1.pairingChannelId)(serviceOrigin, code.nameplate),
44
+ initiatorAd: textEncoder.encode("source"),
45
+ responderAd: textEncoder.encode("destination"),
46
+ });
47
+ return {
48
+ share: (0, keys_1.encodeBase64)(session.share),
49
+ finish(remoteShare) {
50
+ const isk = session.finish((0, keys_1.decodeBase64)(remoteShare));
51
+ const keys = derivePairingSessionKeys(isk);
52
+ isk.fill(0);
53
+ return keys;
54
+ },
55
+ };
36
56
  }
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);
57
+ /** Derives independent signaling and confirmation keys from a PAKE intermediate session key. */
58
+ function derivePairingSessionKeys(isk) {
59
+ return {
60
+ signalingKey: (0, keys_1.encodeBase64)(computeHmac(isk, constants_1.DEVICE_PAIRING_SIGNALING_LABEL)),
61
+ confirmKey: (0, keys_1.encodeBase64)(computeHmac(isk, constants_1.DEVICE_PAIRING_CONFIRM_LABEL)),
62
+ };
40
63
  }
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));
64
+ /**
65
+ * Derives the token a source presents (`auth.token`) when it connects to the destination's direct
66
+ * WebSocket pairing listener. Both endpoints compute it from the signaling key, so it never
67
+ * travels through the rendezvous; a socket without it is refused before any Connection exists.
68
+ */
69
+ function derivePairingWebSocketToken(signalingKey) {
70
+ return (0, keys_1.encodeBase64)(computeHmac((0, keys_1.decodeBase64)(signalingKey), constants_1.DEVICE_PAIRING_WEBSOCKET_TOKEN_LABEL));
44
71
  }
45
72
  /** Computes the MAC over the canonical unsigned signaling envelope. */
46
73
  function computePairingSignalMac(signal, signalingKey) {
@@ -56,31 +83,45 @@ function isPairingSignalMacValid(signal, mac, signalingKey) {
56
83
  return false;
57
84
  }
58
85
  }
59
- /** Creates a signed, expiring approval for one pairing transcript. */
60
- function createSignedPairingApproval(device, role, transcriptHash, expiresAt) {
61
- const contents = types_1.pairingApprovalContentsSchema.parse({
86
+ /** Builds the local `pairing.confirm` message authenticated with the PAKE confirmation key. */
87
+ function createPairingConfirmation(contents, confirmKey) {
88
+ const parsed = types_1.pairingConfirmationContentsSchema.parse({
62
89
  version: constants_1.DEVICE_PAIRING_PROTOCOL_VERSION,
63
- approvalId: (0, keys_1.newToken)(32),
64
- transcriptHash,
65
- role,
66
- expiresAt,
90
+ ...contents,
91
+ });
92
+ return types_1.pairingConfirmationSchema.parse({
93
+ ...parsed,
94
+ mac: (0, keys_1.encodeBase64)(computeHmac((0, keys_1.decodeBase64)(confirmKey), (0, keys_1.stableStringify)(parsed))),
67
95
  });
68
- return types_1.signedPairingApprovalSchema.parse(device.signObjectWithSecretKey(contents));
69
96
  }
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");
97
+ /**
98
+ * Verifies the remote confirmation's MAC, role, transcript, and identity binding.
99
+ * A MAC failure means the other endpoint used a different code or saw a different transcript.
100
+ */
101
+ function verifyPairingConfirmation(value, confirmKey, expected) {
102
+ const parsed = types_1.pairingConfirmationSchema.parse(value);
103
+ const { mac, ...contents } = parsed;
104
+ const expectedMac = computeHmac((0, keys_1.decodeBase64)(confirmKey), (0, keys_1.stableStringify)(contents));
105
+ let macValid = false;
106
+ try {
107
+ macValid = equalBytes(expectedMac, (0, keys_1.decodeBase64)(mac));
75
108
  }
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");
109
+ catch {
110
+ macValid = false;
79
111
  }
80
- if (contents.expiresAt < (expected.now ?? Date.now())) {
81
- throw new Error("Pairing approval has expired");
112
+ if (!macValid) {
113
+ throw new Error("Pairing code did not match");
114
+ }
115
+ if (contents.role !== expected.role || contents.transcriptHash !== expected.transcriptHash) {
116
+ throw new Error("Pairing confirmation does not match this ceremony");
117
+ }
118
+ if (contents.device.userId !== expected.device.userId ||
119
+ contents.device.deviceId !== expected.device.deviceId ||
120
+ contents.device.publicKey !== expected.device.publicKey ||
121
+ contents.device.publicBoxKey !== expected.device.publicBoxKey) {
122
+ throw new Error("Pairing confirmation identity mismatch");
82
123
  }
83
- return contents.approvalId;
124
+ return parsed;
84
125
  }
85
126
  /** Signs and encrypts account credentials directly to the temporary destination identity. */
86
127
  function createPairingCredentialEnvelope(sourceDevice, destinationDevice, credentials, transcriptHash, expiresAt) {