@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
@@ -5,137 +5,186 @@ const keys_1 = require("../keys");
5
5
  const utils_1 = require("../utils");
6
6
  const _1 = require(".");
7
7
  const NOW = 1_800_000_000_000;
8
+ const ORIGIN = "https://peers.app";
8
9
  const FINGERPRINT = Array.from({ length: 32 }, (_, index) => index.toString(16).padStart(2, "0"))
9
10
  .join(":")
10
11
  .toUpperCase();
11
- describe("device pairing invitation", () => {
12
- it("round-trips a versioned token and fragment URL", () => {
13
- const invitation = (0, _1.createDevicePairingInvitation)({
14
- serviceOrigin: "https://peers.app/path",
15
- now: NOW,
16
- });
17
- const token = (0, _1.encodeDevicePairingInvitation)(invitation);
18
- const url = (0, _1.createDevicePairingInvitationUrl)("https://app.peers.test/setup", invitation);
19
- expect((0, _1.parseDevicePairingInvitation)(token, NOW)).toEqual(invitation);
20
- expect((0, _1.parseDevicePairingInvitation)(url, NOW)).toEqual(invitation);
21
- expect(invitation.roomId).toHaveLength(22);
22
- expect(invitation.roomSecret).toHaveLength(43);
23
- expect(invitation.serviceOrigin).toBe("https://peers.app");
12
+ describe("pairing code", () => {
13
+ it("uses the full PGP word lists with no duplicates", () => {
14
+ expect(_1.PGP_EVEN_WORDS).toHaveLength(256);
15
+ expect(_1.PGP_ODD_WORDS).toHaveLength(256);
16
+ expect(new Set([..._1.PGP_EVEN_WORDS, ..._1.PGP_ODD_WORDS]).size).toBe(512);
24
17
  });
25
- it("rejects malformed, expired, unknown-version, and overlong invitations", () => {
26
- const invitation = (0, _1.createDevicePairingInvitation)({
27
- serviceOrigin: "https://peers.app",
28
- now: NOW,
18
+ it("generates even/odd/even words and round-trips through formatting and parsing", () => {
19
+ const words = (0, _1.generatePairingWords)();
20
+ expect(_1.PGP_EVEN_WORDS).toContain(words[0]);
21
+ expect(_1.PGP_ODD_WORDS).toContain(words[1]);
22
+ expect(_1.PGP_EVEN_WORDS).toContain(words[2]);
23
+ const text = (0, _1.formatPairingCode)(7, words);
24
+ expect(text).toBe(`7-${words.join("-")}`);
25
+ expect((0, _1.parsePairingCode)(text)).toEqual({ nameplate: 7, words });
26
+ expect((0, _1.parsePairingCode)(` 7 ${words.join(" ").toUpperCase()} `)).toEqual({
27
+ nameplate: 7,
28
+ words,
29
29
  });
30
- expect(() => (0, _1.parseDevicePairingInvitation)("not-an-invitation", NOW)).toThrow();
31
- expect(() => (0, _1.parseDevicePairingInvitation)((0, _1.encodeDevicePairingInvitation)(invitation), NOW + 300_001)).toThrow();
32
- expect(() => (0, _1.parseDevicePairingInvitation)((0, _1.encodeDevicePairingInvitation)({ ...invitation, version: 2 }), NOW)).toThrow();
33
- expect(() => (0, _1.createDevicePairingInvitation)({
34
- serviceOrigin: "https://peers.app",
35
- now: NOW,
36
- ttlMs: 300_001,
37
- })).toThrow();
38
30
  });
39
- it("accepts only the configured bounded future clock skew", () => {
40
- const skewedInvitation = (0, _1.createDevicePairingInvitation)({
41
- serviceOrigin: "https://peers.app",
42
- now: NOW + _1.DEVICE_PAIRING_CLOCK_SKEW_MS,
43
- });
44
- expect((0, _1.parseDevicePairingInvitation)((0, _1.encodeDevicePairingInvitation)(skewedInvitation), NOW)).toEqual(skewedInvitation);
45
- expect(() => (0, _1.parseDevicePairingInvitation)((0, _1.encodeDevicePairingInvitation)({
46
- ...skewedInvitation,
47
- expiresAt: skewedInvitation.expiresAt + 1,
48
- }), NOW)).toThrow();
31
+ it("rejects wrong slots, unknown words, bad nameplates, and wrong lengths", () => {
32
+ expect(() => (0, _1.parsePairingCode)("7-adroitness-aardvark-absurd")).toThrow(/word 1/);
33
+ expect(() => (0, _1.parsePairingCode)("7-aardvark-absurd-acme")).toThrow(/word 2/);
34
+ expect(() => (0, _1.parsePairingCode)("7-aardvark-adroitness-nope")).toThrow(/word 3/);
35
+ expect(() => (0, _1.parsePairingCode)("0-aardvark-adroitness-acme")).toThrow(/room number/);
36
+ expect(() => (0, _1.parsePairingCode)("10000-aardvark-adroitness-acme")).toThrow(/room number/);
37
+ expect(() => (0, _1.parsePairingCode)("aardvark-adroitness-acme")).toThrow(/three words/);
38
+ expect(() => (0, _1.parsePairingCode)("7-aardvark-adroitness")).toThrow(/three words/);
49
39
  });
50
40
  });
51
- describe("device pairing signaling", () => {
52
- it("authenticates signaling and rejects modification, role substitution, and replay", () => {
53
- const invitation = (0, _1.createDevicePairingInvitation)({
54
- serviceOrigin: "https://peers.app",
55
- now: NOW,
56
- });
57
- const signalingKey = (0, _1.derivePairingSignalingKey)(invitation.roomSecret);
58
- const offerSdp = `v=0\r\na=fingerprint:sha-256 ${FINGERPRINT}\r\n`;
59
- const signal = (0, _1.signPairingSignal)({
60
- version: 1,
61
- roomId: invitation.roomId,
41
+ describe("pairing PAKE and signaling", () => {
42
+ const code = (0, _1.parsePairingCode)("42-aardvark-adroitness-acme");
43
+ it("derives matching keys for the same code and different keys for a wrong code", () => {
44
+ const source = (0, _1.startPairingPake)("source", code, ORIGIN);
45
+ const destination = (0, _1.startPairingPake)("destination", code, ORIGIN);
46
+ const sourceKeys = source.finish(destination.share);
47
+ const destinationKeys = destination.finish(source.share);
48
+ expect(sourceKeys).toEqual(destinationKeys);
49
+ expect(sourceKeys.signalingKey).not.toBe(sourceKeys.confirmKey);
50
+ const token = (0, _1.derivePairingWebSocketToken)(sourceKeys.signalingKey);
51
+ expect(token).toBe((0, _1.derivePairingWebSocketToken)(destinationKeys.signalingKey));
52
+ expect(token).not.toBe(sourceKeys.signalingKey);
53
+ expect(token).not.toBe(sourceKeys.confirmKey);
54
+ const wrong = (0, _1.startPairingPake)("source", (0, _1.parsePairingCode)("42-absurd-adroitness-acme"), ORIGIN);
55
+ const honest = (0, _1.startPairingPake)("destination", code, ORIGIN);
56
+ expect(wrong.finish(honest.share)).not.toEqual(honest.finish(wrong.share));
57
+ const otherRoom = (0, _1.startPairingPake)("source", { ...code, nameplate: 43 }, ORIGIN);
58
+ const thisRoom = (0, _1.startPairingPake)("destination", code, ORIGIN);
59
+ expect(otherRoom.finish(thisRoom.share)).not.toEqual(thisRoom.finish(otherRoom.share));
60
+ });
61
+ it("accepts the pake share unauthenticated and everything else only with the key", () => {
62
+ const share = (0, _1.startPairingPake)("source", code, ORIGIN).share;
63
+ const pake = (0, _1.createPairingPakeSignal)({
64
+ nameplate: 42,
62
65
  senderRole: "source",
63
66
  sequence: 1,
64
- kind: "offer",
67
+ share,
68
+ });
69
+ expect((0, _1.verifyPairingSignal)(pake, undefined, {
70
+ nameplate: 42,
71
+ senderRole: "source",
72
+ nextSequence: 1,
73
+ })).toEqual(pake);
74
+ const keys = (0, _1.startPairingPake)("destination", code, ORIGIN);
75
+ const signalingKey = keys.finish(share).signalingKey;
76
+ const options = (0, _1.signPairingSignal)({
77
+ version: 3,
78
+ nameplate: 42,
79
+ senderRole: "destination",
80
+ sequence: 2,
81
+ kind: "transport-options",
65
82
  payload: {
66
- type: "offer",
67
- sdp: offerSdp,
68
83
  endpointNonce: (0, keys_1.newToken)(32),
84
+ webrtc: false,
85
+ websocket: { urls: ["http://192.168.1.20:3341", "wss://peers.example.com"] },
69
86
  },
70
87
  }, signalingKey);
71
- expect((0, _1.verifyPairingSignal)(signal, signalingKey, {
72
- roomId: invitation.roomId,
73
- senderRole: "source",
74
- nextSequence: 1,
75
- })).toEqual(signal);
76
- expect(() => (0, _1.verifyPairingSignal)({ ...signal, payload: { ...signal.payload, sdp: `${offerSdp}modified` } }, signalingKey, {
77
- roomId: invitation.roomId,
78
- senderRole: "source",
79
- nextSequence: 1,
80
- })).toThrow();
81
- expect(() => (0, _1.verifyPairingSignal)(signal, signalingKey, {
82
- roomId: invitation.roomId,
88
+ expect((0, _1.verifyPairingSignal)(options, signalingKey, {
89
+ nameplate: 42,
83
90
  senderRole: "destination",
84
- nextSequence: 1,
85
- })).toThrow();
86
- expect(() => (0, _1.verifyPairingSignal)(signal, signalingKey, {
87
- roomId: invitation.roomId,
91
+ nextSequence: 2,
92
+ })).toEqual(options);
93
+ expect(() => (0, _1.signPairingSignal)({
94
+ ...options,
95
+ payload: { ...options.payload, websocket: { urls: ["ftp://nope"] } },
96
+ }, signalingKey)).toThrow();
97
+ const choice = (0, _1.signPairingSignal)({
98
+ version: 3,
99
+ nameplate: 42,
100
+ senderRole: "source",
101
+ sequence: 2,
102
+ kind: "transport-choice",
103
+ payload: { endpointNonce: (0, keys_1.newToken)(32), selected: "ws", url: "http://192.168.1.20:3341" },
104
+ }, signalingKey);
105
+ expect((0, _1.verifyPairingSignal)(choice, signalingKey, {
106
+ nameplate: 42,
88
107
  senderRole: "source",
89
108
  nextSequence: 2,
90
- })).toThrow();
109
+ })).toEqual(choice);
110
+ const offerSdp = `v=0\r\na=fingerprint:sha-256 ${FINGERPRINT}\r\n`;
111
+ const signal = (0, _1.signPairingSignal)({
112
+ version: 3,
113
+ nameplate: 42,
114
+ senderRole: "source",
115
+ sequence: 3,
116
+ kind: "offer",
117
+ payload: { type: "offer", sdp: offerSdp },
118
+ }, signalingKey);
119
+ const expected = { nameplate: 42, senderRole: "source", nextSequence: 3 };
120
+ expect((0, _1.verifyPairingSignal)(signal, signalingKey, expected)).toEqual(signal);
121
+ expect(() => (0, _1.verifyPairingSignal)(signal, undefined, expected)).toThrow(/before authentication/);
122
+ expect(() => (0, _1.verifyPairingSignal)({ ...signal, payload: { ...signal.payload, sdp: `${offerSdp}modified` } }, signalingKey, expected)).toThrow(/authentication failed/);
123
+ expect(() => (0, _1.verifyPairingSignal)(signal, signalingKey, { ...expected, senderRole: "destination" })).toThrow(/out of order/);
124
+ expect(() => (0, _1.verifyPairingSignal)(signal, signalingKey, { ...expected, nextSequence: 4 })).toThrow(/out of order/);
125
+ expect(() => (0, _1.verifyPairingSignal)(signal, signalingKey, { ...expected, nameplate: 41 })).toThrow(/out of order/);
91
126
  });
92
127
  });
93
- describe("device pairing transcript and credentials", () => {
94
- it("derives a stable transcript SAS that changes with SDP", () => {
95
- const invitation = (0, _1.createDevicePairingInvitation)({
96
- serviceOrigin: "https://peers.app",
97
- now: NOW,
98
- });
99
- const source = new device_1.Device();
100
- const destination = new device_1.Device();
101
- const transcript = {
102
- version: 1,
103
- roomId: invitation.roomId,
104
- sourceIdentity: (0, keys_1.openSignedObject)(source.getDeviceInfo()),
105
- destinationIdentity: (0, keys_1.openSignedObject)(destination.getDeviceInfo()),
106
- sourceEndpointNonce: (0, keys_1.newToken)(32),
107
- destinationEndpointNonce: (0, keys_1.newToken)(32),
108
- offerFingerprint: (0, _1.extractSha256SdpFingerprint)(`v=0\r\na=fingerprint:sha-256 ${FINGERPRINT}\r\n`),
109
- answerFingerprint: (0, _1.extractSha256SdpFingerprint)(`v=0\r\na=fingerprint:sha-256 ${FINGERPRINT}\r\n`),
110
- };
128
+ describe("pairing transcript, confirmation, and credentials", () => {
129
+ const source = new device_1.Device();
130
+ const destination = new device_1.Device();
131
+ const sdp = `v=0\r\na=fingerprint:sha-256 ${FINGERPRINT}\r\n`;
132
+ const transcript = {
133
+ version: 3,
134
+ serviceOrigin: ORIGIN,
135
+ nameplate: 42,
136
+ sourceIdentity: (0, keys_1.openSignedObject)(source.getDeviceInfo()),
137
+ destinationIdentity: (0, keys_1.openSignedObject)(destination.getDeviceInfo()),
138
+ sourceEndpointNonce: (0, keys_1.newToken)(32),
139
+ destinationEndpointNonce: (0, keys_1.newToken)(32),
140
+ transport: "wrtc",
141
+ transportBinding: (0, _1.webRtcPairingTransportBinding)(sdp, sdp),
142
+ };
143
+ it("hashes the transcript deterministically and changes with any input", () => {
111
144
  const hash = (0, _1.computeDevicePairingTranscriptHash)(transcript);
112
- const sas = (0, _1.deriveDevicePairingSas)(invitation.roomSecret, hash);
113
- const changedHash = (0, _1.computeDevicePairingTranscriptHash)({
145
+ expect((0, _1.computeDevicePairingTranscriptHash)(transcript)).toBe(hash);
146
+ expect((0, _1.computeDevicePairingTranscriptHash)({ ...transcript, nameplate: 43 })).not.toBe(hash);
147
+ expect(transcript.transportBinding).toBe(`${(0, _1.extractSha256SdpFingerprint)(sdp)}|${(0, _1.extractSha256SdpFingerprint)(sdp)}`);
148
+ expect((0, _1.computeDevicePairingTranscriptHash)({
114
149
  ...transcript,
115
- answerFingerprint: FINGERPRINT.replace("00", "FF"),
116
- });
117
- expect(sas).toMatch(/^\d{6}$/);
118
- expect((0, _1.deriveDevicePairingSas)(invitation.roomSecret, hash)).toBe(sas);
119
- expect((0, _1.deriveDevicePairingSas)(invitation.roomSecret, changedHash)).not.toBe(sas);
150
+ transportBinding: transcript.transportBinding.replace("00", "FF"),
151
+ })).not.toBe(hash);
152
+ expect((0, _1.computeDevicePairingTranscriptHash)({
153
+ ...transcript,
154
+ transport: "ws",
155
+ transportBinding: "http://192.168.1.20:3341",
156
+ })).not.toBe(hash);
120
157
  });
121
- it("requires the expected identities and transcript for approval, credentials, and receipt", () => {
122
- const source = new device_1.Device();
123
- const destination = new device_1.Device();
124
- const transcriptHash = (0, keys_1.newToken)(32);
125
- const expiresAt = NOW + 60_000;
126
- const approval = (0, _1.createSignedPairingApproval)(destination, "destination", transcriptHash, expiresAt);
127
- expect((0, _1.verifySignedPairingApproval)(approval, {
128
- publicKey: destination.publicKey,
129
- transcriptHash,
158
+ it("confirms only with the same key, role, transcript, and identity", () => {
159
+ const code = (0, _1.parsePairingCode)("42-aardvark-adroitness-acme");
160
+ const sourcePake = (0, _1.startPairingPake)("source", code, ORIGIN);
161
+ const destinationPake = (0, _1.startPairingPake)("destination", code, ORIGIN);
162
+ const keys = sourcePake.finish(destinationPake.share);
163
+ const wrongKeys = (0, _1.startPairingPake)("destination", (0, _1.parsePairingCode)("42-absurd-adroitness-acme"), ORIGIN).finish((0, _1.startPairingPake)("source", code, ORIGIN).share);
164
+ const transcriptHash = (0, _1.computeDevicePairingTranscriptHash)(transcript);
165
+ const confirmation = (0, _1.createPairingConfirmation)({
130
166
  role: "destination",
131
- now: NOW,
132
- })).toHaveLength(43);
133
- expect(() => (0, _1.verifySignedPairingApproval)(approval, {
134
- publicKey: source.publicKey,
135
167
  transcriptHash,
168
+ device: transcript.destinationIdentity,
169
+ presentation: { deviceName: "Phone", deviceType: "mobile" },
170
+ }, keys.confirmKey);
171
+ const expected = {
136
172
  role: "destination",
137
- now: NOW,
138
- })).toThrow();
173
+ transcriptHash,
174
+ device: transcript.destinationIdentity,
175
+ };
176
+ expect((0, _1.verifyPairingConfirmation)(confirmation, keys.confirmKey, expected)).toEqual(confirmation);
177
+ expect(() => (0, _1.verifyPairingConfirmation)(confirmation, wrongKeys.confirmKey, expected)).toThrow(/did not match/);
178
+ expect(() => (0, _1.verifyPairingConfirmation)(confirmation, keys.confirmKey, { ...expected, role: "source" })).toThrow(/does not match/);
179
+ expect(() => (0, _1.verifyPairingConfirmation)(confirmation, keys.confirmKey, {
180
+ ...expected,
181
+ device: transcript.sourceIdentity,
182
+ })).toThrow(/identity mismatch/);
183
+ expect(() => (0, _1.verifyPairingConfirmation)({ ...confirmation, presentation: { deviceName: "Evil" } }, keys.confirmKey, expected)).toThrow(/did not match/);
184
+ });
185
+ it("requires the expected identities and transcript for credentials and receipt", () => {
186
+ const transcriptHash = (0, keys_1.newToken)(32);
187
+ const expiresAt = NOW + 60_000;
139
188
  const credentials = { userId: (0, utils_1.newid)(), secretKey: (0, keys_1.newKeys)().secretKey };
140
189
  const destinationInfo = (0, keys_1.openSignedObject)(destination.getDeviceInfo());
141
190
  const envelope = (0, _1.createPairingCredentialEnvelope)(source, destinationInfo, credentials, transcriptHash, expiresAt);
@@ -144,12 +193,7 @@ describe("device pairing transcript and credentials", () => {
144
193
  sourcePublicBoxKey: source.publicBoxKey,
145
194
  transcriptHash,
146
195
  now: NOW,
147
- })).toEqual({
148
- version: 1,
149
- transcriptHash,
150
- expiresAt,
151
- ...credentials,
152
- });
196
+ })).toEqual({ version: 3, transcriptHash, expiresAt, ...credentials });
153
197
  expect(() => (0, _1.openPairingCredentialEnvelope)(new device_1.Device(), envelope, {
154
198
  sourcePublicKey: source.publicKey,
155
199
  sourcePublicBoxKey: source.publicBoxKey,
@@ -164,11 +208,4 @@ describe("device pairing transcript and credentials", () => {
164
208
  transcriptHash,
165
209
  })).toEqual(initializedInfo);
166
210
  });
167
- it("derives independent room-access and signaling credentials", () => {
168
- const invitation = (0, _1.createDevicePairingInvitation)({
169
- serviceOrigin: "https://peers.app",
170
- now: NOW,
171
- });
172
- expect((0, _1.derivePairingRoomAccessToken)(invitation.roomSecret)).not.toBe((0, _1.derivePairingSignalingKey)(invitation.roomSecret));
173
- });
174
211
  });
@@ -1,6 +1,8 @@
1
1
  export * from "./constants";
2
+ export * from "./cpace";
2
3
  export * from "./crypto";
3
- export * from "./invitation";
4
+ export * from "./pairing-code";
5
+ export * from "./pgp-word-list";
4
6
  export * from "./signaling";
5
7
  export * from "./transcript";
6
8
  export * from "./types";
@@ -15,8 +15,10 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./constants"), exports);
18
+ __exportStar(require("./cpace"), exports);
18
19
  __exportStar(require("./crypto"), exports);
19
- __exportStar(require("./invitation"), exports);
20
+ __exportStar(require("./pairing-code"), exports);
21
+ __exportStar(require("./pgp-word-list"), exports);
20
22
  __exportStar(require("./signaling"), exports);
21
23
  __exportStar(require("./transcript"), exports);
22
24
  __exportStar(require("./types"), exports);
@@ -0,0 +1,27 @@
1
+ /** Number of words in a pairing code; each carries one full random byte (24 bits total). */
2
+ export declare const PAIRING_CODE_WORD_COUNT = 3;
3
+ /** The three password words of a pairing code, drawn even/odd/even from the PGP word list. */
4
+ export type PairingWords = readonly [string, string, string];
5
+ /** Parsed `<nameplate>-<word>-<word>-<word>` pairing code. */
6
+ export interface PairingCode {
7
+ /** Small integer selecting the rendezvous room; not part of the PAKE password. */
8
+ nameplate: number;
9
+ /** Lowercase canonical words that form the PAKE password. */
10
+ words: PairingWords;
11
+ }
12
+ /** Samples three words, one uniformly random byte each, alternating even/odd/even lists. */
13
+ export declare function generatePairingWords(): PairingWords;
14
+ /** Formats the code shown to the user and encoded in the QR: `7-guitar-revenge-tunnel`. */
15
+ export declare function formatPairingCode(nameplate: number, words: PairingWords): string;
16
+ /**
17
+ * Parses typed or scanned input. Case-insensitive; hyphen or whitespace separated; a word
18
+ * from the wrong list for its position is rejected immediately (before any network use).
19
+ * @throws with a generic message that never echoes the input
20
+ */
21
+ export declare function parsePairingCode(text: string): PairingCode;
22
+ /** PAKE password bytes: the canonical lowercase words joined by hyphens (nameplate excluded). */
23
+ export declare function pairingPasswordFromCode(code: PairingCode): Uint8Array;
24
+ /** PAKE channel identifier binding the exchange to one rendezvous service and room. */
25
+ export declare function pairingChannelId(serviceOrigin: string, nameplate: number): Uint8Array;
26
+ /** Returns whether a nameplate is a positive integer within the service pool. */
27
+ export declare function isValidNameplate(value: number): boolean;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PAIRING_CODE_WORD_COUNT = void 0;
4
+ exports.generatePairingWords = generatePairingWords;
5
+ exports.formatPairingCode = formatPairingCode;
6
+ exports.parsePairingCode = parsePairingCode;
7
+ exports.pairingPasswordFromCode = pairingPasswordFromCode;
8
+ exports.pairingChannelId = pairingChannelId;
9
+ exports.isValidNameplate = isValidNameplate;
10
+ const nacl = require("tweetnacl");
11
+ const constants_1 = require("./constants");
12
+ const cpace_1 = require("./cpace");
13
+ const pgp_word_list_1 = require("./pgp-word-list");
14
+ /** Number of words in a pairing code; each carries one full random byte (24 bits total). */
15
+ exports.PAIRING_CODE_WORD_COUNT = 3;
16
+ const textEncoder = new TextEncoder();
17
+ const EVEN_INDEX = new Map(pgp_word_list_1.PGP_EVEN_WORDS.map((word, index) => [word, index]));
18
+ const ODD_INDEX = new Map(pgp_word_list_1.PGP_ODD_WORDS.map((word, index) => [word, index]));
19
+ function wordListForPosition(position) {
20
+ return position % 2 === 0 ? pgp_word_list_1.PGP_EVEN_WORDS : pgp_word_list_1.PGP_ODD_WORDS;
21
+ }
22
+ function wordIndexForPosition(position) {
23
+ return position % 2 === 0 ? EVEN_INDEX : ODD_INDEX;
24
+ }
25
+ /** Samples three words, one uniformly random byte each, alternating even/odd/even lists. */
26
+ function generatePairingWords() {
27
+ const bytes = nacl.randomBytes(exports.PAIRING_CODE_WORD_COUNT);
28
+ return [
29
+ wordListForPosition(0)[bytes[0]],
30
+ wordListForPosition(1)[bytes[1]],
31
+ wordListForPosition(2)[bytes[2]],
32
+ ];
33
+ }
34
+ /** Formats the code shown to the user and encoded in the QR: `7-guitar-revenge-tunnel`. */
35
+ function formatPairingCode(nameplate, words) {
36
+ assertNameplate(nameplate);
37
+ return [String(nameplate), ...words].join("-");
38
+ }
39
+ /**
40
+ * Parses typed or scanned input. Case-insensitive; hyphen or whitespace separated; a word
41
+ * from the wrong list for its position is rejected immediately (before any network use).
42
+ * @throws with a generic message that never echoes the input
43
+ */
44
+ function parsePairingCode(text) {
45
+ const parts = text
46
+ .trim()
47
+ .toLowerCase()
48
+ .split(/[\s-]+/)
49
+ .filter((part) => part.length > 0);
50
+ if (parts.length !== exports.PAIRING_CODE_WORD_COUNT + 1) {
51
+ throw new Error("Pairing code must be a number followed by three words");
52
+ }
53
+ const nameplate = Number.parseInt(parts[0], 10);
54
+ if (!/^\d+$/.test(parts[0]) || !isValidNameplate(nameplate)) {
55
+ throw new Error("Pairing code has an invalid room number");
56
+ }
57
+ const words = [];
58
+ for (let position = 0; position < exports.PAIRING_CODE_WORD_COUNT; position++) {
59
+ const word = parts[position + 1];
60
+ if (!wordIndexForPosition(position).has(word)) {
61
+ throw new Error(`Pairing code word ${position + 1} is not recognized`);
62
+ }
63
+ words.push(word);
64
+ }
65
+ return { nameplate, words: words };
66
+ }
67
+ /** PAKE password bytes: the canonical lowercase words joined by hyphens (nameplate excluded). */
68
+ function pairingPasswordFromCode(code) {
69
+ return textEncoder.encode(code.words.join("-"));
70
+ }
71
+ /** PAKE channel identifier binding the exchange to one rendezvous service and room. */
72
+ function pairingChannelId(serviceOrigin, nameplate) {
73
+ assertNameplate(nameplate);
74
+ return (0, cpace_1.cpaceLvCat)(textEncoder.encode("peers-device-pairing"), textEncoder.encode(serviceOrigin), textEncoder.encode(String(nameplate)));
75
+ }
76
+ /** Returns whether a nameplate is a positive integer within the service pool. */
77
+ function isValidNameplate(value) {
78
+ return Number.isInteger(value) && value >= 1 && value <= constants_1.DEVICE_PAIRING_MAX_NAMEPLATE;
79
+ }
80
+ function assertNameplate(value) {
81
+ if (!isValidNameplate(value)) {
82
+ throw new Error("Invalid pairing nameplate");
83
+ }
84
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The PGP word list: 256 two-syllable "even" words and 256 three-syllable "odd" words.
3
+ *
4
+ * Word `n` in each list encodes the byte value `n`. Pairing codes alternate lists by
5
+ * position (even, odd, even) so a word typed in the wrong slot is detected before the
6
+ * PAKE runs. The list was designed for reading aloud: words are phonetically distinct
7
+ * and stress-pattern distinct across the two lists.
8
+ *
9
+ * Source: https://en.wikipedia.org/wiki/PGP_word_list (lowercased).
10
+ */
11
+ /** Two-syllable words used for byte positions 0 and 2 of a pairing code. */
12
+ export declare const PGP_EVEN_WORDS: readonly string[];
13
+ /** Three-syllable words used for byte position 1 of a pairing code. */
14
+ export declare const PGP_ODD_WORDS: readonly string[];