Skip to content

Commit 2464a69

Browse files
hughnsturt2liveKerry
authored
Support sign in + E2EE set up using QR code implementing MSC3886, MSC3903 and MSC3906 (#2747)
* Clean implementation of MSC3886 and MSC3903 * Refactor to use object initialiser instead of lots of args + handle non-compliant fetch better * Start of some unit tests * Make AES work on Node.js as well as browser * Tests for ECDH/X25519 * stric mode linting * Fix incorrect test * Refactor full rendezvous logic out of react-sdk into js-sdk * Use correct unstable import * Pass fetch around * Make correct usage of fetch in tests * fix: you can't call fetch when it's not on window * Use class names to make it clearer that these are unstable MSC implementations * Linting * Clean implementation of MSC3886 and MSC3903 * Refactor to use object initialiser instead of lots of args + handle non-compliant fetch better * Start of some unit tests * Make AES work on Node.js as well as browser * Tests for ECDH/X25519 * stric mode linting * Fix incorrect test * Refactor full rendezvous logic out of react-sdk into js-sdk * Use correct unstable import * Pass fetch around * Make correct usage of fetch in tests * fix: you can't call fetch when it's not on window * Use class names to make it clearer that these are unstable MSC implementations * Linting * Reduce log noise * Tidy up interface a bit * Additional test for transport layer * Linting * Refactor dummy transport to be re-usable * Remove redundant condition * Handle more error cases * Initial tests for MSC3906 * Reduce scope of PR to only cover generating a code on existing device * Strict linting * Additional test cases * Lint * additional test cases and remove some code smells * More test cases * Strict lint * Strict lint * Test case * Refactor to handle UIA * Unstable prefixes * Lint * Missed due to lack of strict... * Test server capabilities using Feature * Remove redundant assignment * Refactor ro resuse generateDecimal from SAS * Update src/rendezvous/transports/simpleHttpTransport.ts Co-authored-by: Travis Ralston <[email protected]> * Update src/rendezvous/transports/simpleHttpTransport.ts Co-authored-by: Travis Ralston <[email protected]> * Update src/rendezvous/channels/ecdhV1.ts Co-authored-by: Travis Ralston <[email protected]> * Update src/rendezvous/transports/simpleHttpTransport.ts Co-authored-by: Travis Ralston <[email protected]> * Rename files to titlecase * Visibility modifiers * Resolve public mutability * Refactor logic to reduce duplication * Refactor to have better defined data types throughout * Rebase and remove Node.js crypto * Wipe AES key out after use * Add typing for MSC3906 layer * Strict lint * Fix double connect detection * Remove unintended debug statement * Return types * Use generics * Make type of MSC3903ECDHPayload explicit * Use unstable prefix for RendezvousChannelAlgorithm * Fix * Extra unstable type * Test types Co-authored-by: Travis Ralston <[email protected]> Co-authored-by: Kerry <[email protected]>
1 parent 7ffdf17 commit 2464a69

19 files changed

+2357
-16
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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 { logger } from "../../../src/logger";
18+
import {
19+
RendezvousFailureListener,
20+
RendezvousFailureReason,
21+
RendezvousTransport,
22+
RendezvousTransportDetails,
23+
} from "../../../src/rendezvous";
24+
import { sleep } from '../../../src/utils';
25+
26+
export class DummyTransport<D extends RendezvousTransportDetails, T> implements RendezvousTransport<T> {
27+
otherParty?: DummyTransport<D, T>;
28+
etag?: string;
29+
lastEtagReceived?: string;
30+
data: T | undefined;
31+
32+
ready = false;
33+
cancelled = false;
34+
35+
constructor(private name: string, private mockDetails: D) {}
36+
onCancelled?: RendezvousFailureListener;
37+
38+
details(): Promise<RendezvousTransportDetails> {
39+
return Promise.resolve(this.mockDetails);
40+
}
41+
42+
async send(data: T): Promise<void> {
43+
logger.info(
44+
`[${this.name}] => [${this.otherParty?.name}] Attempting to send data: ${
45+
JSON.stringify(data)} where etag matches ${this.etag}`,
46+
);
47+
// eslint-disable-next-line no-constant-condition
48+
while (!this.cancelled) {
49+
if (!this.etag || (this.otherParty?.etag && this.otherParty?.etag === this.etag)) {
50+
this.data = data;
51+
this.etag = Math.random().toString();
52+
this.lastEtagReceived = this.etag;
53+
this.otherParty!.etag = this.etag;
54+
this.otherParty!.data = data;
55+
logger.info(`[${this.name}] => [${this.otherParty?.name}] Sent with etag ${this.etag}`);
56+
return;
57+
}
58+
logger.info(`[${this.name}] Sleeping to retry send after etag ${this.etag}`);
59+
await sleep(250);
60+
}
61+
}
62+
63+
async receive(): Promise<T | undefined> {
64+
logger.info(`[${this.name}] Attempting to receive where etag is after ${this.lastEtagReceived}`);
65+
// eslint-disable-next-line no-constant-condition
66+
while (!this.cancelled) {
67+
if (!this.lastEtagReceived || this.lastEtagReceived !== this.etag) {
68+
this.lastEtagReceived = this.etag;
69+
logger.info(
70+
`[${this.otherParty?.name}] => [${this.name}] Received data: ` +
71+
`${JSON.stringify(this.data)} with etag ${this.etag}`,
72+
);
73+
return this.data;
74+
}
75+
logger.info(`[${this.name}] Sleeping to retry receive after etag ${
76+
this.lastEtagReceived} as remote is ${this.etag}`);
77+
await sleep(250);
78+
}
79+
80+
return undefined;
81+
}
82+
83+
cancel(reason: RendezvousFailureReason): Promise<void> {
84+
this.cancelled = true;
85+
this.onCancelled?.(reason);
86+
return Promise.resolve();
87+
}
88+
89+
cleanup() {
90+
this.cancelled = true;
91+
}
92+
}

spec/unit/rendezvous/ecdh.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, MSC3903ECDHv1RendezvousChannel } 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('ECDHv1', 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 MSC3903ECDHv1RendezvousChannel(aliceTransport);
41+
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
42+
const bob = new MSC3903ECDHv1RendezvousChannel(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 MSC3903ECDHv1RendezvousChannel(aliceTransport);
66+
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
67+
const bob = new MSC3903ECDHv1RendezvousChannel(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 MSC3903ECDHv1RendezvousChannel(aliceTransport);
91+
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
92+
const bob = new MSC3903ECDHv1RendezvousChannel(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 MSC3903ECDHv1RendezvousChannel(aliceTransport);
113+
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
114+
const bob = new MSC3903ECDHv1RendezvousChannel(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 MSC3903ECDHv1RendezvousChannel(aliceTransport);
139+
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
140+
const bob = new MSC3903ECDHv1RendezvousChannel(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.toThrowError();
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 MSC3903ECDHv1RendezvousChannel(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.toThrowError();
168+
169+
await alice.cancel(RendezvousFailureReason.Unknown);
170+
});
171+
});
172+
});

0 commit comments

Comments
 (0)