Skip to content

Commit

Permalink
Basic implementation of SAS verification in Rust
Browse files Browse the repository at this point in the history
  • Loading branch information
richvdh committed Jun 20, 2023
1 parent 50775c5 commit ea9914d
Show file tree
Hide file tree
Showing 7 changed files with 522 additions and 23 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.10",
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.11",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
Expand Down
49 changes: 35 additions & 14 deletions spec/integ/crypto/verification.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ limitations under the License.

import fetchMock from "fetch-mock-jest";
import { MockResponse } from "fetch-mock";
import "fake-indexeddb/auto";

import { createClient, CryptoEvent, MatrixClient } from "../../../src";
import {
Expand Down Expand Up @@ -49,7 +50,7 @@ jest.useFakeTimers();

let previousCrypto: Crypto | undefined;

beforeAll(() => {
beforeAll(async () => {
// Stub out global.crypto
previousCrypto = global["crypto"];

Expand All @@ -61,6 +62,9 @@ beforeAll(() => {
},
},
});

// we use the libolm primitives in the test, so init the Olm library
await global.Olm.init();
});

// restore the original global.crypto
Expand Down Expand Up @@ -136,6 +140,10 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u

afterEach(async () => {
await aliceClient.stopClient();

// Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync();

fetchMock.mockReset();
});

Expand All @@ -145,7 +153,9 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA);
});

oldBackendOnly("can verify via SAS", async () => {
it("can verify another device via SAS", async () => {
await waitForDeviceList();

// have alice initiate a verification. She should send a m.key.verification.request
let [requestBody, request] = await Promise.all([
expectSendToDeviceMessage("m.key.verification.request"),
Expand Down Expand Up @@ -207,11 +217,12 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
expect(verifier.getShowSasCallbacks()).toBeNull();

// start off the verification process: alice will send an `accept`
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
const verificationPromise = verifier.verify();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
jest.advanceTimersByTime(10);

requestBody = await expectSendToDeviceMessage("m.key.verification.accept");
requestBody = await sendToDevicePromise;
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
expect(toDeviceMessage.key_agreement_protocol).toEqual("curve25519-hkdf-sha256");
expect(toDeviceMessage.short_authentication_string).toEqual(["decimal", "emoji"]);
Expand Down Expand Up @@ -288,15 +299,9 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
});

oldBackendOnly("can verify another via QR code with an untrusted cross-signing key", async () => {
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);

// QRCode fails if we don't yet have the cross-signing keys, so make sure we have them now.
//
// Completing the initial sync will make the device list download outdated device lists (of which our own
// user will be one).
syncResponder.sendOrQueueSyncResponse({});
// DeviceList has a sleep(5) which we need to make happen
await jest.advanceTimersByTimeAsync(10);
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
await waitForDeviceList();
expect(aliceClient.getStoredCrossSigningForUser(TEST_USER_ID)).toBeTruthy();

// have alice initiate a verification. She should send a m.key.verification.request
Expand Down Expand Up @@ -384,7 +389,9 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
expect(request.phase).toEqual(VerificationPhase.Done);
});

oldBackendOnly("can cancel during the SAS phase", async () => {
it("can cancel during the SAS phase", async () => {
await waitForDeviceList();

// have alice initiate a verification. She should send a m.key.verification.request
const [, request] = await Promise.all([
expectSendToDeviceMessage("m.key.verification.request"),
Expand Down Expand Up @@ -426,12 +433,13 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
expect(verifier.hasBeenCancelled).toBe(false);

// start off the verification process: alice will send an `accept`
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
const verificationPromise = verifier.verify();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
jest.advanceTimersByTime(10);
await expectSendToDeviceMessage("m.key.verification.accept");
await sendToDevicePromise;

// now we unceremoniously cancel
// now we unceremoniously cancel. We expect the verificatationPromise to reject.
const requestPromise = expectSendToDeviceMessage("m.key.verification.cancel");
verifier.cancel(new Error("blah"));
await requestPromise;
Expand Down Expand Up @@ -486,6 +494,19 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
});
});

/** make sure that the client knows about the dummy device */
async function waitForDeviceList(): Promise<void> {
// Completing the initial sync will make the device list download outdated device lists (of which our own
// user will be one).
syncResponder.sendOrQueueSyncResponse({});
// DeviceList has a sleep(5) which we need to make happen
await jest.advanceTimersByTimeAsync(10);

// The client should now know about the dummy device
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
expect(devices.get(TEST_USER_ID)!.keys()).toContain(TEST_DEVICE_ID);
}

function returnToDeviceMessageFromSync(ev: { type: string; content: object; sender?: string }): void {
ev.sender ??= TEST_USER_ID;
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } });
Expand Down
9 changes: 9 additions & 0 deletions spec/unit/rust-crypto/rust-crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,15 @@ describe("RustCrypto", () => {
expect(deviceMap.has(testData.TEST_DEVICE_ID)).toBe(true);
rustCrypto.stop();
});

describe("requestDeviceVerification", () => {
it("throws an error if the device is unknown", async () => {
const rustCrypto = await makeTestRustCrypto();
await expect(() => rustCrypto.requestDeviceVerification(TEST_USER, "unknown")).rejects.toThrow(
"Not a known device",
);
});
});
});

/** build a basic RustCrypto instance for testing
Expand Down
2 changes: 2 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2235,6 +2235,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.secretStorage,
this.cryptoCallbacks,
);
rustCrypto.supportedVerificationMethods = this.verificationMethods;

this.cryptoBackend = rustCrypto;

// attach the event listeners needed by RustCrypto
Expand Down
41 changes: 37 additions & 4 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { secretStorageContainsCrossSigningKeys } from "./secret-storage";
import { keyFromPassphrase } from "../crypto/key_passphrase";
import { encodeRecoveryKey } from "../crypto/recoverykey";
import { crypto } from "../crypto/crypto";
import { RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification";

/**
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
Expand Down Expand Up @@ -508,6 +509,13 @@ export class RustCrypto implements CryptoBackend {
return;
}

/**
* The verification methods we offer to the other side during an interactive verification.
*
* If `undefined`, we will offer all the methods supported by the Rust SDK.
*/
public supportedVerificationMethods: string[] | undefined;

/**
* Send a verification request to our other devices.
*
Expand All @@ -517,7 +525,18 @@ export class RustCrypto implements CryptoBackend {
*
* @returns a VerificationRequest when the request has been sent to the other party.
*/
public requestOwnUserVerification(): Promise<VerificationRequest> {
public async requestOwnUserVerification(): Promise<VerificationRequest> {
/* something like this, but currently untested
const user: RustSdkCryptoJs.OwnUserIdentity = await this.olmMachine.getIdentity(
new RustSdkCryptoJs.UserId(this.userId),
);
const [request, outgoingRequest]: [RustSdkCryptoJs.VerificationRequest, RustSdkCryptoJs.ToDeviceRequest] =
await user.requestVerification(
this.supportedVerificationMethods?.map(verificationMethodIdentifierToMethod),
);
await this.outgoingRequestProcessor.makeOutgoingRequest(outgoingRequest);
return new RustVerificationRequest(request, this.outgoingRequestProcessor);
*/
throw new Error("not implemented");
}

Expand All @@ -526,15 +545,29 @@ export class RustCrypto implements CryptoBackend {
*
* If a verification is already in flight, returns it. Otherwise, initiates a new one.
*
* Implementation of {@link CryptoApi#requestDeviceVerification }.
* Implementation of {@link CryptoApi#requestDeviceVerification}.
*
* @param userId - ID of the owner of the device to verify
* @param deviceId - ID of the device to verify
*
* @returns a VerificationRequest when the request has been sent to the other party.
*/
public requestDeviceVerification(userId: string, deviceId: string): Promise<VerificationRequest> {
throw new Error("not implemented");
public async requestDeviceVerification(userId: string, deviceId: string): Promise<VerificationRequest> {
const device: RustSdkCryptoJs.Device | undefined = await this.olmMachine.getDevice(
new RustSdkCryptoJs.UserId(userId),
new RustSdkCryptoJs.DeviceId(deviceId),
);

if (!device) {
throw new Error("Not a known device");
}

const [request, outgoingRequest]: [RustSdkCryptoJs.VerificationRequest, RustSdkCryptoJs.ToDeviceRequest] =
await device.requestVerification(
this.supportedVerificationMethods?.map(verificationMethodIdentifierToMethod),
);
await this.outgoingRequestProcessor.makeOutgoingRequest(outgoingRequest);
return new RustVerificationRequest(request, this.outgoingRequestProcessor);
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down
Loading

0 comments on commit ea9914d

Please sign in to comment.