@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.
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +4 -1
- package/dist/contracts/system/index.d.ts +2 -0
- package/dist/contracts/system/index.js +5 -1
- package/dist/contracts/system/invites-contract.d.ts +121 -0
- package/dist/contracts/system/invites-contract.js +150 -0
- package/dist/data/create-group.d.ts +24 -0
- package/dist/data/create-group.js +61 -0
- package/dist/data/group-secret.d.ts +10 -0
- package/dist/data/group-secret.js +26 -0
- package/dist/data/group-share.d.ts +0 -20
- package/dist/data/group-share.js +0 -35
- package/dist/data/index.d.ts +2 -0
- package/dist/data/index.js +2 -0
- package/dist/data/invites.d.ts +350 -0
- package/dist/data/invites.js +99 -0
- package/dist/data/users.js +4 -0
- package/dist/device-pairing/constants.d.ts +27 -9
- package/dist/device-pairing/constants.js +28 -10
- package/dist/device-pairing/cpace.d.ts +46 -0
- package/dist/device-pairing/cpace.js +173 -0
- package/dist/device-pairing/cpace.test.js +116 -0
- package/dist/device-pairing/crypto.d.ts +40 -15
- package/dist/device-pairing/crypto.js +74 -33
- package/dist/device-pairing/device-pairing.test.js +156 -119
- package/dist/device-pairing/index.d.ts +3 -1
- package/dist/device-pairing/index.js +3 -1
- package/dist/device-pairing/pairing-code.d.ts +27 -0
- package/dist/device-pairing/pairing-code.js +84 -0
- package/dist/device-pairing/pgp-word-list.d.ts +14 -0
- package/dist/device-pairing/pgp-word-list.js +531 -0
- package/dist/device-pairing/signaling.d.ts +17 -6
- package/dist/device-pairing/signaling.js +31 -8
- package/dist/device-pairing/transcript.d.ts +3 -3
- package/dist/device-pairing/transcript.js +6 -13
- package/dist/device-pairing/types.d.ts +870 -224
- package/dist/device-pairing/types.js +186 -108
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/invites/data-box-schema.d.ts +15 -0
- package/dist/invites/data-box-schema.js +15 -0
- package/dist/invites/group-approval.d.ts +43 -0
- package/dist/invites/group-approval.js +155 -0
- package/dist/invites/group-approval.test.js +221 -0
- package/dist/invites/index.d.ts +5 -0
- package/dist/{group-invite → invites}/index.js +5 -3
- package/dist/invites/invite-epoch.d.ts +18 -0
- package/dist/invites/invite-epoch.js +35 -0
- package/dist/invites/invite-messages.d.ts +1354 -0
- package/dist/invites/invite-messages.js +145 -0
- package/dist/invites/invite-token.d.ts +334 -0
- package/dist/invites/invite-token.js +178 -0
- package/dist/invites/invite-token.test.d.ts +1 -0
- package/dist/invites/invite-token.test.js +123 -0
- package/dist/rpc-types.d.ts +5 -4
- package/dist/rpc-types.js +2 -1
- package/dist/system-ids.d.ts +22 -0
- package/dist/system-ids.js +23 -1
- package/dist/types/peer-device.d.ts +15 -1
- package/dist/types/peer-device.js +15 -1
- package/package.json +1 -1
- package/dist/device-pairing/invitation.d.ts +0 -13
- package/dist/device-pairing/invitation.js +0 -85
- package/dist/group-invite/group-invite.d.ts +0 -53
- package/dist/group-invite/group-invite.js +0 -231
- package/dist/group-invite/group-invite.pvars.d.ts +0 -94
- package/dist/group-invite/group-invite.pvars.js +0 -139
- package/dist/group-invite/group-invite.types.d.ts +0 -124
- package/dist/group-invite/group-invite.types.js +0 -8
- package/dist/group-invite/index.d.ts +0 -3
- package/dist/user-connect/connection-code.d.ts +0 -67
- package/dist/user-connect/connection-code.js +0 -176
- package/dist/user-connect/connection-code.test.js +0 -213
- package/dist/user-connect/index.d.ts +0 -3
- package/dist/user-connect/index.js +0 -19
- package/dist/user-connect/user-connect.pvars.d.ts +0 -20
- package/dist/user-connect/user-connect.pvars.js +0 -63
- package/dist/user-connect/user-connect.pvars.test.js +0 -46
- package/dist/user-connect/user-connect.types.d.ts +0 -94
- package/dist/user-connect/user-connect.types.js +0 -68
- /package/dist/{user-connect/connection-code.test.d.ts → device-pairing/cpace.test.d.ts} +0 -0
- /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("
|
|
12
|
-
it("
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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("
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
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("
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
expect((
|
|
45
|
-
expect(() => (0, _1.
|
|
46
|
-
|
|
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("
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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)(
|
|
72
|
-
|
|
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:
|
|
85
|
-
})).
|
|
86
|
-
expect(() => (0, _1.
|
|
87
|
-
|
|
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
|
-
})).
|
|
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("
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
116
|
-
});
|
|
117
|
-
expect(
|
|
118
|
-
|
|
119
|
-
|
|
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("
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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 "./
|
|
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("./
|
|
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[];
|