Skip to content

Commit 4932030

Browse files
authored
Support for v2 of MSC3903 (#3155)
* v2 of MSC3903 implementation This is a deliberate breaking change on an unstable feature. * Test correct protocol version * Fix up test * v2 of MSC3903 implementation This is a deliberate breaking change on an unstable feature. * Test correct protocol version * Fix up test * Reinstate v1 support to make this a non-breaking change Deprecates several experimental types
1 parent 41782c4 commit 4932030

File tree

6 files changed

+465
-35
lines changed

6 files changed

+465
-35
lines changed

spec/unit/rendezvous/ecdhv2.spec.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import "../../olm-loader";
18+
import { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
19+
import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "../../../src/rendezvous/channels";
20+
import { decodeBase64 } from "../../../src/crypto/olmlib";
21+
import { DummyTransport } from "./DummyTransport";
22+
23+
function makeTransport(name: string) {
24+
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: "dummy" });
25+
}
26+
27+
describe("ECDHv2", function () {
28+
beforeAll(async function () {
29+
await global.Olm.init();
30+
});
31+
32+
describe("with crypto", () => {
33+
it("initiator wants to sign in", async function () {
34+
const aliceTransport = makeTransport("Alice");
35+
const bobTransport = makeTransport("Bob");
36+
aliceTransport.otherParty = bobTransport;
37+
bobTransport.otherParty = aliceTransport;
38+
39+
// alice is signing in initiates and generates a code
40+
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
41+
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
42+
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
43+
44+
const bobChecksum = await bob.connect();
45+
const aliceChecksum = await alice.connect();
46+
47+
expect(aliceChecksum).toEqual(bobChecksum);
48+
49+
const message = { key: "xxx" };
50+
await alice.send(message);
51+
const bobReceive = await bob.receive();
52+
expect(bobReceive).toEqual(message);
53+
54+
await alice.cancel(RendezvousFailureReason.Unknown);
55+
await bob.cancel(RendezvousFailureReason.Unknown);
56+
});
57+
58+
it("initiator wants to reciprocate", async function () {
59+
const aliceTransport = makeTransport("Alice");
60+
const bobTransport = makeTransport("Bob");
61+
aliceTransport.otherParty = bobTransport;
62+
bobTransport.otherParty = aliceTransport;
63+
64+
// alice is signing in initiates and generates a code
65+
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
66+
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
67+
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
68+
69+
const bobChecksum = await bob.connect();
70+
const aliceChecksum = await alice.connect();
71+
72+
expect(aliceChecksum).toEqual(bobChecksum);
73+
74+
const message = { key: "xxx" };
75+
await bob.send(message);
76+
const aliceReceive = await alice.receive();
77+
expect(aliceReceive).toEqual(message);
78+
79+
await alice.cancel(RendezvousFailureReason.Unknown);
80+
await bob.cancel(RendezvousFailureReason.Unknown);
81+
});
82+
83+
it("double connect", async function () {
84+
const aliceTransport = makeTransport("Alice");
85+
const bobTransport = makeTransport("Bob");
86+
aliceTransport.otherParty = bobTransport;
87+
bobTransport.otherParty = aliceTransport;
88+
89+
// alice is signing in initiates and generates a code
90+
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
91+
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
92+
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
93+
94+
const bobChecksum = await bob.connect();
95+
const aliceChecksum = await alice.connect();
96+
97+
expect(aliceChecksum).toEqual(bobChecksum);
98+
99+
expect(alice.connect()).rejects.toThrow();
100+
101+
await alice.cancel(RendezvousFailureReason.Unknown);
102+
await bob.cancel(RendezvousFailureReason.Unknown);
103+
});
104+
105+
it("closed", async function () {
106+
const aliceTransport = makeTransport("Alice");
107+
const bobTransport = makeTransport("Bob");
108+
aliceTransport.otherParty = bobTransport;
109+
bobTransport.otherParty = aliceTransport;
110+
111+
// alice is signing in initiates and generates a code
112+
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
113+
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
114+
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
115+
116+
const bobChecksum = await bob.connect();
117+
const aliceChecksum = await alice.connect();
118+
119+
expect(aliceChecksum).toEqual(bobChecksum);
120+
121+
alice.close();
122+
123+
expect(alice.connect()).rejects.toThrow();
124+
expect(alice.send({})).rejects.toThrow();
125+
expect(alice.receive()).rejects.toThrow();
126+
127+
await alice.cancel(RendezvousFailureReason.Unknown);
128+
await bob.cancel(RendezvousFailureReason.Unknown);
129+
});
130+
131+
it("require ciphertext", async function () {
132+
const aliceTransport = makeTransport("Alice");
133+
const bobTransport = makeTransport("Bob");
134+
aliceTransport.otherParty = bobTransport;
135+
bobTransport.otherParty = aliceTransport;
136+
137+
// alice is signing in initiates and generates a code
138+
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
139+
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
140+
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
141+
142+
const bobChecksum = await bob.connect();
143+
const aliceChecksum = await alice.connect();
144+
145+
expect(aliceChecksum).toEqual(bobChecksum);
146+
147+
// send a message without encryption
148+
await aliceTransport.send({ iv: "dummy", ciphertext: "dummy" });
149+
expect(bob.receive()).rejects.toThrow();
150+
151+
await alice.cancel(RendezvousFailureReason.Unknown);
152+
await bob.cancel(RendezvousFailureReason.Unknown);
153+
});
154+
155+
it("ciphertext before set up", async function () {
156+
const aliceTransport = makeTransport("Alice");
157+
const bobTransport = makeTransport("Bob");
158+
aliceTransport.otherParty = bobTransport;
159+
bobTransport.otherParty = aliceTransport;
160+
161+
// alice is signing in initiates and generates a code
162+
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
163+
await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
164+
165+
await bobTransport.send({ iv: "dummy", ciphertext: "dummy" });
166+
167+
expect(alice.receive()).rejects.toThrow();
168+
169+
await alice.cancel(RendezvousFailureReason.Unknown);
170+
});
171+
});
172+
});

spec/unit/rendezvous/rendezvous.spec.ts

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ import MockHttpBackend from "matrix-mock-request";
1919
import "../../olm-loader";
2020
import { MSC3906Rendezvous, RendezvousCode, RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
2121
import {
22-
ECDHv1RendezvousCode,
22+
ECDHv2RendezvousCode as ECDHRendezvousCode,
2323
MSC3903ECDHPayload,
24-
MSC3903ECDHv1RendezvousChannel,
24+
MSC3903ECDHv2RendezvousChannel as MSC3903ECDHRendezvousChannel,
2525
} from "../../../src/rendezvous/channels";
2626
import { MatrixClient } from "../../../src";
2727
import {
@@ -126,7 +126,7 @@ describe("Rendezvous", function () {
126126
fallbackRzServer: "https://fallbackserver/rz",
127127
fetchFn,
128128
});
129-
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
129+
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport);
130130
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
131131

132132
expect(aliceRz.code).toBeUndefined();
@@ -143,7 +143,7 @@ describe("Rendezvous", function () {
143143
const code = JSON.parse(aliceRz.code!) as RendezvousCode;
144144

145145
expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE);
146-
expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256");
146+
expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256");
147147
expect(code.rendezvous?.transport.type).toEqual("org.matrix.msc3886.http.v1");
148148
expect((code.rendezvous?.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri).toEqual(
149149
"https://fallbackserver/rz/123",
@@ -181,19 +181,19 @@ describe("Rendezvous", function () {
181181
msc3882Enabled: false,
182182
msc3886Enabled: false,
183183
});
184-
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
184+
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
185185
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
186186
aliceTransport.onCancelled = aliceOnFailure;
187187
await aliceRz.generateCode();
188-
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
188+
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
189189

190190
expect(code.rendezvous.key).toBeDefined();
191191

192192
const aliceStartProm = aliceRz.startAfterShowingCode();
193193

194194
// bob is try to sign in and scans the code
195195
const bobOnFailure = jest.fn();
196-
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
196+
const bobEcdh = new MSC3903ECDHRendezvousChannel(
197197
bobTransport,
198198
decodeBase64(code.rendezvous.key), // alice's public key
199199
bobOnFailure,
@@ -235,19 +235,19 @@ describe("Rendezvous", function () {
235235
msc3882Enabled: true,
236236
msc3886Enabled: false,
237237
});
238-
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
238+
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
239239
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
240240
aliceTransport.onCancelled = aliceOnFailure;
241241
await aliceRz.generateCode();
242-
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
242+
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
243243

244244
expect(code.rendezvous.key).toBeDefined();
245245

246246
const aliceStartProm = aliceRz.startAfterShowingCode();
247247

248248
// bob is try to sign in and scans the code
249249
const bobOnFailure = jest.fn();
250-
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
250+
const bobEcdh = new MSC3903ECDHRendezvousChannel(
251251
bobTransport,
252252
decodeBase64(code.rendezvous.key), // alice's public key
253253
bobOnFailure,
@@ -293,19 +293,19 @@ describe("Rendezvous", function () {
293293
msc3882Enabled: true,
294294
msc3886Enabled: false,
295295
});
296-
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
296+
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
297297
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
298298
aliceTransport.onCancelled = aliceOnFailure;
299299
await aliceRz.generateCode();
300-
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
300+
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
301301

302302
expect(code.rendezvous.key).toBeDefined();
303303

304304
const aliceStartProm = aliceRz.startAfterShowingCode();
305305

306306
// bob is try to sign in and scans the code
307307
const bobOnFailure = jest.fn();
308-
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
308+
const bobEcdh = new MSC3903ECDHRendezvousChannel(
309309
bobTransport,
310310
decodeBase64(code.rendezvous.key), // alice's public key
311311
bobOnFailure,
@@ -351,19 +351,19 @@ describe("Rendezvous", function () {
351351
msc3882Enabled: true,
352352
msc3886Enabled: false,
353353
});
354-
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
354+
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
355355
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
356356
aliceTransport.onCancelled = aliceOnFailure;
357357
await aliceRz.generateCode();
358-
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
358+
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
359359

360360
expect(code.rendezvous.key).toBeDefined();
361361

362362
const aliceStartProm = aliceRz.startAfterShowingCode();
363363

364364
// bob is try to sign in and scans the code
365365
const bobOnFailure = jest.fn();
366-
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
366+
const bobEcdh = new MSC3903ECDHRendezvousChannel(
367367
bobTransport,
368368
decodeBase64(code.rendezvous.key), // alice's public key
369369
bobOnFailure,
@@ -411,19 +411,19 @@ describe("Rendezvous", function () {
411411
msc3882Enabled: true,
412412
msc3886Enabled: false,
413413
});
414-
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
414+
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
415415
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
416416
aliceTransport.onCancelled = aliceOnFailure;
417417
await aliceRz.generateCode();
418-
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
418+
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
419419

420420
expect(code.rendezvous.key).toBeDefined();
421421

422422
const aliceStartProm = aliceRz.startAfterShowingCode();
423423

424424
// bob is try to sign in and scans the code
425425
const bobOnFailure = jest.fn();
426-
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
426+
const bobEcdh = new MSC3903ECDHRendezvousChannel(
427427
bobTransport,
428428
decodeBase64(code.rendezvous.key), // alice's public key
429429
bobOnFailure,
@@ -485,19 +485,19 @@ describe("Rendezvous", function () {
485485
master: "mmmmm",
486486
},
487487
});
488-
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
488+
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
489489
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
490490
aliceTransport.onCancelled = aliceOnFailure;
491491
await aliceRz.generateCode();
492-
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
492+
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
493493

494494
expect(code.rendezvous.key).toBeDefined();
495495

496496
const aliceStartProm = aliceRz.startAfterShowingCode();
497497

498498
// bob is try to sign in and scans the code
499499
const bobOnFailure = jest.fn();
500-
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
500+
const bobEcdh = new MSC3903ECDHRendezvousChannel(
501501
bobTransport,
502502
decodeBase64(code.rendezvous.key), // alice's public key
503503
bobOnFailure,

src/rendezvous/channels/MSC3903ECDHv1RendezvousChannel.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,19 @@ import { encodeBase64, decodeBase64 } from "../../crypto/olmlib";
2929
import { crypto, subtleCrypto, TextEncoder } from "../../crypto/crypto";
3030
import { generateDecimalSas } from "../../crypto/verification/SASDecimal";
3131
import { UnstableValue } from "../../NamespacedValue";
32+
import { EncryptedPayload, MSC3903ECDHPayload, PlainTextPayload } from "./MSC3903ECDHv2RendezvousChannel";
3233

33-
const ECDH_V1 = new UnstableValue(
34+
/**
35+
* @deprecated Use ECDH_V2 instead
36+
*/
37+
export const ECDH_V1 = new UnstableValue(
3438
"m.rendezvous.v1.curve25519-aes-sha256",
3539
"org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256",
3640
);
3741

42+
/**
43+
* @deprecated Use ECDHv2RendezvousCode instead
44+
*/
3845
export interface ECDHv1RendezvousCode extends RendezvousCode {
3946
rendezvous: {
4047
transport: RendezvousTransportDetails;
@@ -43,18 +50,6 @@ export interface ECDHv1RendezvousCode extends RendezvousCode {
4350
};
4451
}
4552

46-
export type MSC3903ECDHPayload = PlainTextPayload | EncryptedPayload;
47-
48-
export interface PlainTextPayload {
49-
algorithm: typeof ECDH_V1.name | typeof ECDH_V1.altName;
50-
key?: string;
51-
}
52-
53-
export interface EncryptedPayload {
54-
iv: string;
55-
ciphertext: string;
56-
}
57-
5853
async function importKey(key: Uint8Array): Promise<CryptoKey> {
5954
if (!subtleCrypto) {
6055
throw new Error("Web Crypto is not available");
@@ -69,6 +64,8 @@ async function importKey(key: Uint8Array): Promise<CryptoKey> {
6964
* Implementation of the unstable [MSC3903](https://github.com/matrix-org/matrix-spec-proposals/pull/3903)
7065
* X25519/ECDH key agreement based secure rendezvous channel.
7166
* Note that this is UNSTABLE and may have breaking changes without notice.
67+
*
68+
* @deprecated Use MSC3903ECDHv2RendezvousChannel instead. This implementation will be removed.
7269
*/
7370
export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
7471
private olmSAS?: SAS;

0 commit comments

Comments
 (0)