Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support sign in + E2EE set up using QR code implementing MSC3886, MSC3903 and MSC3906 #2747

Merged
merged 79 commits into from
Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from 70 commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
9e48662
Clean implementation of MSC3886 and MSC3903
hughns Oct 12, 2022
9513b29
Refactor to use object initialiser instead of lots of args + handle n…
hughns Oct 12, 2022
d167030
Start of some unit tests
hughns Oct 12, 2022
9e29014
Make AES work on Node.js as well as browser
hughns Oct 12, 2022
e164875
Tests for ECDH/X25519
hughns Oct 12, 2022
f6eb641
stric mode linting
hughns Oct 12, 2022
b9b923e
Fix incorrect test
hughns Oct 12, 2022
5e27b8c
Refactor full rendezvous logic out of react-sdk into js-sdk
hughns Oct 12, 2022
be3c2a7
Use correct unstable import
hughns Oct 12, 2022
a98c7aa
Pass fetch around
hughns Oct 12, 2022
ceaa489
Make correct usage of fetch in tests
hughns Oct 13, 2022
6eb04e0
fix: you can't call fetch when it's not on window
hughns Oct 13, 2022
c8f260a
Use class names to make it clearer that these are unstable MSC implem…
hughns Oct 13, 2022
93ad97c
Linting
hughns Oct 13, 2022
06566e1
Clean implementation of MSC3886 and MSC3903
hughns Oct 12, 2022
7462dd1
Refactor to use object initialiser instead of lots of args + handle n…
hughns Oct 12, 2022
9eed493
Start of some unit tests
hughns Oct 12, 2022
b04bc70
Make AES work on Node.js as well as browser
hughns Oct 12, 2022
18f7383
Tests for ECDH/X25519
hughns Oct 12, 2022
c126680
stric mode linting
hughns Oct 12, 2022
145aaa0
Fix incorrect test
hughns Oct 12, 2022
14e4a76
Refactor full rendezvous logic out of react-sdk into js-sdk
hughns Oct 12, 2022
adb0a5e
Use correct unstable import
hughns Oct 12, 2022
707f486
Pass fetch around
hughns Oct 12, 2022
4b72466
Make correct usage of fetch in tests
hughns Oct 13, 2022
ccf7b73
fix: you can't call fetch when it's not on window
hughns Oct 13, 2022
da2f024
Use class names to make it clearer that these are unstable MSC implem…
hughns Oct 13, 2022
0853f31
Linting
hughns Oct 13, 2022
4f39e3d
Merge branch 'hughns/login-with-qr' of https://github.com/matrix-org/…
hughns Oct 13, 2022
aaf5ca2
Reduce log noise
hughns Oct 13, 2022
4d37fc2
Tidy up interface a bit
hughns Oct 13, 2022
d64cd99
Additional test for transport layer
hughns Oct 13, 2022
22e4fe8
Linting
hughns Oct 13, 2022
a81b70e
Refactor dummy transport to be re-usable
hughns Oct 13, 2022
33b31d0
Remove redundant condition
hughns Oct 14, 2022
9d3a6da
Handle more error cases
hughns Oct 14, 2022
4bf9080
Initial tests for MSC3906
hughns Oct 14, 2022
0fc0608
Reduce scope of PR to only cover generating a code on existing device
hughns Oct 15, 2022
c92a5f0
Strict linting
hughns Oct 15, 2022
f1324b5
Additional test cases
hughns Oct 15, 2022
f8a738d
Lint
hughns Oct 15, 2022
3f53046
additional test cases and remove some code smells
hughns Oct 16, 2022
6924a0d
Merge branch 'develop' into hughns/login-with-qr
hughns Oct 16, 2022
42ad64f
More test cases
hughns Oct 16, 2022
6e1a557
Strict lint
hughns Oct 16, 2022
85929e0
Strict lint
hughns Oct 16, 2022
150c147
Test case
hughns Oct 16, 2022
61f50d9
Refactor to handle UIA
hughns Oct 16, 2022
ea6f7f2
Unstable prefixes
hughns Oct 17, 2022
493f1d8
Lint
hughns Oct 17, 2022
5db266b
Missed due to lack of strict...
hughns Oct 17, 2022
42b6259
Test server capabilities using Feature
hughns Oct 17, 2022
737791c
Remove redundant assignment
hughns Oct 17, 2022
4b00b15
Refactor ro resuse generateDecimal from SAS
hughns Oct 17, 2022
a5de008
Update src/rendezvous/transports/simpleHttpTransport.ts
hughns Oct 17, 2022
76ccd98
Update src/rendezvous/transports/simpleHttpTransport.ts
hughns Oct 17, 2022
c9f9032
Update src/rendezvous/channels/ecdhV1.ts
hughns Oct 17, 2022
4b313db
Update src/rendezvous/transports/simpleHttpTransport.ts
hughns Oct 17, 2022
bacd2bf
Rename files to titlecase
hughns Oct 17, 2022
e3260b0
Visibility modifiers
hughns Oct 17, 2022
5ef415f
Resolve public mutability
hughns Oct 17, 2022
c1c600f
Refactor logic to reduce duplication
hughns Oct 17, 2022
91bd005
Refactor to have better defined data types throughout
hughns Oct 17, 2022
020d735
Merge branch 'develop' into hughns/login-with-qr
hughns Oct 17, 2022
ceeecad
Rebase and remove Node.js crypto
hughns Oct 17, 2022
1f6838a
Wipe AES key out after use
hughns Oct 17, 2022
d8bb398
Add typing for MSC3906 layer
hughns Oct 17, 2022
3c33b45
Strict lint
hughns Oct 17, 2022
3319ca4
Fix double connect detection
hughns Oct 18, 2022
3cd46cd
Remove unintended debug statement
hughns Oct 18, 2022
f8a130b
Return types
hughns Oct 18, 2022
c6291a7
Use generics
hughns Oct 18, 2022
710401d
Make type of MSC3903ECDHPayload explicit
hughns Oct 18, 2022
87fca05
Use unstable prefix for RendezvousChannelAlgorithm
hughns Oct 18, 2022
1dff0f4
Fix
hughns Oct 18, 2022
4e0ff75
Extra unstable type
hughns Oct 18, 2022
a067213
Test types
hughns Oct 18, 2022
590c46a
Merge branch 'develop' into hughns/login-with-qr
hughns Oct 19, 2022
bf6f3e8
Merge branch 'develop' into hughns/login-with-qr
Oct 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions spec/unit/rendezvous/DummyTransport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { logger } from "../../../src/logger";
import {
RendezvousFailureListener,
RendezvousFailureReason,
RendezvousTransport,
RendezvousTransportDetails,
} from "../../../src/rendezvous";
import { sleep } from '../../../src/utils';

export class DummyTransport<T extends RendezvousTransportDetails> implements RendezvousTransport {
otherParty?: DummyTransport<T>;
etag?: string;
lastEtagReceived?: string;
data: object = {};

ready = false;
cancelled = false;

constructor(private name: string, private mockDetails: T) {}
onCancelled?: RendezvousFailureListener;

details(): Promise<RendezvousTransportDetails> {
return Promise.resolve(this.mockDetails);
}

async send(data: object): Promise<void> {
logger.info(
`[${this.name}] => [${this.otherParty?.name}] Attempting to send data: ${
JSON.stringify(data)} where etag matches ${this.etag}`,
);
// eslint-disable-next-line no-constant-condition
while (!this.cancelled) {
if (!this.etag || (this.otherParty?.etag && this.otherParty?.etag === this.etag)) {
this.data = data;
this.etag = Math.random().toString();
this.lastEtagReceived = this.etag;
this.otherParty!.etag = this.etag;
this.otherParty!.data = data;
logger.info(`[${this.name}] => [${this.otherParty?.name}] Sent with etag ${this.etag}`);
return;
}
logger.info(`[${this.name}] Sleeping to retry send after etag ${this.etag}`);
await sleep(250);
}
}

async receive(): Promise<object> {
logger.info(`[${this.name}] Attempting to receive where etag is after ${this.lastEtagReceived}`);
// eslint-disable-next-line no-constant-condition
while (!this.cancelled) {
if (!this.lastEtagReceived || this.lastEtagReceived !== this.etag) {
this.lastEtagReceived = this.etag;
logger.info(
`[${this.otherParty?.name}] => [${this.name}] Received data: ` +
`${JSON.stringify(this.data)} with etag ${this.etag}`,
);
return this.data;
}
logger.info(`[${this.name}] Sleeping to retry receive after etag ${
this.lastEtagReceived} as remote is ${this.etag}`);
await sleep(250);
}

return {};
}

cancel(reason: RendezvousFailureReason): Promise<void> {
this.cancelled = true;
this.onCancelled?.(reason);
return Promise.resolve();
}

cleanup() {
this.cancelled = true;
}
}
168 changes: 168 additions & 0 deletions spec/unit/rendezvous/ecdh.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import '../../olm-loader';
import { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
import { MSC3903ECDHv1RendezvousChannel } from '../../../src/rendezvous/channels';
import { decodeBase64 } from '../../../src/crypto/olmlib';
import { DummyTransport } from './DummyTransport';

describe('ECDHv1', function() {
beforeAll(async function() {
await global.Olm.init();
});

describe('with crypto', () => {
it("initiator wants to sign in", async function() {
const aliceTransport = new DummyTransport('Alice', { type: 'dummy' });
const bobTransport = new DummyTransport('Bob', { type: 'dummy' });
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;

// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));

const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();

expect(aliceChecksum).toEqual(bobChecksum);

const message = { key: "xxx" };
await alice.send(message);
const bobReceive = await bob.receive();
expect(bobReceive).toEqual(message);

await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
});

it("initiator wants to reciprocate", async function() {
const aliceTransport = new DummyTransport('Alice', { type: 'dummy' });
const bobTransport = new DummyTransport('Bob', { type: 'dummy' });
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;

// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));

const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();

expect(aliceChecksum).toEqual(bobChecksum);

const message = { key: "xxx" };
await bob.send(message);
const aliceReceive = await alice.receive();
expect(aliceReceive).toEqual(message);

await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
});

it("double connect", async function() {
const aliceTransport = new DummyTransport('Alice', { type: 'dummy' });
const bobTransport = new DummyTransport('Bob', { type: 'dummy' });
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;

// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));

const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();

expect(aliceChecksum).toEqual(bobChecksum);

expect(alice.connect()).rejects.toThrow();

await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
});

it("closed", async function() {
const aliceTransport = new DummyTransport('Alice', { type: 'dummy' });
const bobTransport = new DummyTransport('Bob', { type: 'dummy' });
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;

// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));

const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();

expect(aliceChecksum).toEqual(bobChecksum);

alice.close();

expect(alice.connect()).rejects.toThrow();
expect(alice.send({})).rejects.toThrow();
expect(alice.receive()).rejects.toThrow();

await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
});

it("require ciphertext", async function() {
const aliceTransport = new DummyTransport('Alice', { type: 'dummy' });
const bobTransport = new DummyTransport('Bob', { type: 'dummy' });
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;

// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));

const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();

expect(aliceChecksum).toEqual(bobChecksum);

// send a message without encryption
await aliceTransport.send({});
expect(bob.receive()).rejects.toThrowError();

await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
});

it("ciphertext before set up", async function() {
const aliceTransport = new DummyTransport('Alice', { type: 'dummy' });
const bobTransport = new DummyTransport('Bob', { type: 'dummy' });
aliceTransport.otherParty = bobTransport;
bobTransport.otherParty = aliceTransport;

// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);

await bobTransport.send({ ciphertext: "foo" });

expect(alice.receive()).rejects.toThrowError();

await alice.cancel(RendezvousFailureReason.Unknown);
});
});
});
Loading