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 19, 2023
1 parent eaff21c commit 5b56923
Show file tree
Hide file tree
Showing 7 changed files with 540 additions and 19 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
64 changes: 55 additions & 9 deletions spec/integ/crypto/verification.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ 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 { createClient, CryptoEvent, IDownloadKeyResult, MatrixClient } from "../../../src";
import {
canAcceptVerificationRequest,
ShowQrCodeCallbacks,
Expand All @@ -40,14 +41,15 @@ import {
TEST_USER_ID,
} from "../../test-utils/test-data";
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";

// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations
// to ensure that we don't end up with dangling timeouts.
jest.useFakeTimers();

let previousCrypto: Crypto | undefined;

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

Expand All @@ -59,6 +61,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 @@ -90,6 +95,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
let syncResponder: SyncResponder;

/** an object which intercepts `/keys/upload` requests from {@link #aliceClient} */
let e2eKeyReceiver: E2EKeyReceiver;

beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
Expand All @@ -108,25 +116,49 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st

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

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

fetchMock.mockReset();
});

beforeEach(() => {
syncResponder = new SyncResponder(aliceClient.getHomeserverUrl());
mockInitialApiRequests(aliceClient.getHomeserverUrl());
e2eKeyReceiver = new E2EKeyReceiver(aliceClient.getHomeserverUrl());
aliceClient.startClient();
});

oldBackendOnly("Outgoing verification: can verify another device via SAS", async () => {
it("Outgoing verification: can verify another device via SAS", async () => {
// expect requests to download our own keys
fetchMock.post(new RegExp("/_matrix/client/(r0|v3)/keys/query"), {
device_keys: {
[TEST_USER_ID]: {
[TEST_DEVICE_ID]: SIGNED_TEST_DEVICE_DATA,
fetchMock.post(new RegExp("/_matrix/client/(r0|v3)/keys/query"), () => {
const response: Pick<IDownloadKeyResult, "device_keys"> = {
device_keys: {
[TEST_USER_ID]: {
[TEST_DEVICE_ID]: SIGNED_TEST_DEVICE_DATA,
},
},
},
};
// if alice has already uploaded her keys, return those too
const aliceKeys = e2eKeyReceiver.getUploadedDeviceKeys();
if (aliceKeys !== null) {
response.device_keys[TEST_USER_ID][aliceKeys.device_id] = aliceKeys;
}
return response;
});

// Rust crypto needs to know about the other device, so let /sync complete, which will kick off the
// outgoing request processor (which will make a keys query).
syncResponder.sendOrQueueSyncResponse({});

// Legacy DeviceList has a sleep(5) which we need to make happen
await jest.advanceTimersByTimeAsync(10);

// The device list should now be up to date
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
expect(devices.get(TEST_USER_ID)!.keys()).toContain(TEST_DEVICE_ID);

// 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 All @@ -136,6 +168,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
expect(transactionId).toBeDefined();
expect(request.phase).toEqual(VerificationPhase.Requested);
expect(request.roomId).toBeUndefined();
expect(request.isSelfVerification).toBe(true);
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(false); // no reply yet

let toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
expect(toDeviceMessage.methods).toContain("m.sas.v1");
Expand Down Expand Up @@ -171,6 +205,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
});
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Started);
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(true);
expect(request.chosenMethod).toEqual("m.sas.v1");

// there should now be a verifier
Expand All @@ -179,14 +214,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
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"]);
const macMethod = toDeviceMessage.message_authentication_code;
expect(macMethod).toEqual("hkdf-hmac-sha256.v2");
expect(toDeviceMessage.transaction_id).toEqual(transactionId);

// The dummy device makes up a curve25519 keypair and sends the public bit back in an `m.key.verification.key'
Expand Down Expand Up @@ -241,6 +279,14 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// that should satisfy Alice, who should reply with a 'done'
await expectSendToDeviceMessage("m.key.verification.done");

// the dummy device also confirms done-ness
returnToDeviceMessageFromSync({
type: "m.key.verification.done",
content: {
transaction_id: transactionId,
},
});

// ... and the whole thing should be done!
await verificationPromise;
expect(request.phase).toEqual(VerificationPhase.Done);
Expand Down
7 changes: 7 additions & 0 deletions spec/test-utils/E2EKeyReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
return this.deviceKeys.keys[keyIds[0]];
}

/**
* If the device keys have already been uploaded, return them. Else return null.
*/
public getUploadedDeviceKeys(): IDeviceKeys | null {
return this.deviceKeys;
}

/**
* If one-time keys have already been uploaded, return them. Otherwise,
* set up an expectation that the keys will be uploaded, and wait for
Expand Down
2 changes: 2 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2229,6 +2229,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// needed.
const RustCrypto = await import("./rust-crypto");
const rustCrypto = await RustCrypto.initRustCrypto(this.http, userId, deviceId, this.secretStorage);
rustCrypto.supportedVerificationMethods = this.verificationMethods;

this.cryptoBackend = rustCrypto;

// attach the event listeners needed by RustCrypto
Expand Down
40 changes: 35 additions & 5 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,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 @@ -460,6 +461,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 @@ -469,24 +477,46 @@ export class RustCrypto implements CryptoBackend {
*
* @returns a VerificationRequest when the request has been sent to the other party.
*/
public requestOwnUserVerification(): Promise<VerificationRequest> {
throw new Error("not implemented");
public async requestOwnUserVerification(): Promise<VerificationRequest> {
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);
}

/**
* Request an interactive verification with the given device.
*
* 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 5b56923

Please sign in to comment.