You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-25 05:23:13 +03:00
Element-R: support for starting a SAS verification (#3528)
* integ tests: factor out some utility methods * Add `VerificationRequest.startVerification` to replace `beginKeyVerification` The rust SDK ties together creating the verifier and sending the `m.key.verification.start` message, so we need to combine `.beginKeyVerification` and `.verify`. * add some unit tests
This commit is contained in:
committed by
GitHub
parent
1828826661
commit
5165899e82
@@ -16,9 +16,11 @@ limitations under the License.
|
|||||||
|
|
||||||
import "fake-indexeddb/auto";
|
import "fake-indexeddb/auto";
|
||||||
|
|
||||||
|
import anotherjson from "another-json";
|
||||||
import { MockResponse } from "fetch-mock";
|
import { MockResponse } from "fetch-mock";
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "fetch-mock-jest";
|
||||||
import { IDBFactory } from "fake-indexeddb";
|
import { IDBFactory } from "fake-indexeddb";
|
||||||
|
import { createHash } from "crypto";
|
||||||
|
|
||||||
import { createClient, CryptoEvent, ICreateClientOpts, MatrixClient } from "../../../src";
|
import { createClient, CryptoEvent, ICreateClientOpts, MatrixClient } from "../../../src";
|
||||||
import {
|
import {
|
||||||
@@ -209,13 +211,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
|||||||
// The dummy device makes up a curve25519 keypair and sends the public bit back in an `m.key.verification.key'
|
// The dummy device makes up a curve25519 keypair and sends the public bit back in an `m.key.verification.key'
|
||||||
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
|
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
|
||||||
const olmSAS = new global.Olm.SAS();
|
const olmSAS = new global.Olm.SAS();
|
||||||
returnToDeviceMessageFromSync({
|
returnToDeviceMessageFromSync(buildSasKeyMessage(transactionId, olmSAS.get_pubkey()));
|
||||||
type: "m.key.verification.key",
|
|
||||||
content: {
|
|
||||||
transaction_id: transactionId,
|
|
||||||
key: olmSAS.get_pubkey(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// alice responds with a 'key' ...
|
// alice responds with a 'key' ...
|
||||||
requestBody = await expectSendToDeviceMessage("m.key.verification.key");
|
requestBody = await expectSendToDeviceMessage("m.key.verification.key");
|
||||||
@@ -239,32 +235,15 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
|||||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||||
|
|
||||||
// the dummy device also confirms that the emoji match, and sends a mac
|
// the dummy device also confirms that the emoji match, and sends a mac
|
||||||
const macInfoBase = `MATRIX_KEY_VERIFICATION_MAC${TEST_USER_ID}${TEST_DEVICE_ID}${TEST_USER_ID}${aliceClient.deviceId}${transactionId}`;
|
returnToDeviceMessageFromSync(
|
||||||
returnToDeviceMessageFromSync({
|
buildSasMacMessage(transactionId, olmSAS, TEST_USER_ID, aliceClient.deviceId!),
|
||||||
type: "m.key.verification.mac",
|
);
|
||||||
content: {
|
|
||||||
keys: calculateMAC(olmSAS, `ed25519:${TEST_DEVICE_ID}`, `${macInfoBase}KEY_IDS`),
|
|
||||||
transaction_id: transactionId,
|
|
||||||
mac: {
|
|
||||||
[`ed25519:${TEST_DEVICE_ID}`]: calculateMAC(
|
|
||||||
olmSAS,
|
|
||||||
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
|
|
||||||
`${macInfoBase}ed25519:${TEST_DEVICE_ID}`,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// that should satisfy Alice, who should reply with a 'done'
|
// that should satisfy Alice, who should reply with a 'done'
|
||||||
await expectSendToDeviceMessage("m.key.verification.done");
|
await expectSendToDeviceMessage("m.key.verification.done");
|
||||||
|
|
||||||
// the dummy device also confirms done-ness
|
// the dummy device also confirms done-ness
|
||||||
returnToDeviceMessageFromSync({
|
returnToDeviceMessageFromSync(buildDoneMessage(transactionId));
|
||||||
type: "m.key.verification.done",
|
|
||||||
content: {
|
|
||||||
transaction_id: transactionId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// ... and the whole thing should be done!
|
// ... and the whole thing should be done!
|
||||||
await verificationPromise;
|
await verificationPromise;
|
||||||
@@ -278,6 +257,102 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
|||||||
olmSAS.free();
|
olmSAS.free();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("can initiate SAS verification ourselves", async () => {
|
||||||
|
aliceClient = await startTestClient();
|
||||||
|
await waitForDeviceList();
|
||||||
|
|
||||||
|
// Alice sends a m.key.verification.request
|
||||||
|
const [, request] = await Promise.all([
|
||||||
|
expectSendToDeviceMessage("m.key.verification.request"),
|
||||||
|
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
|
||||||
|
]);
|
||||||
|
const transactionId = request.transactionId!;
|
||||||
|
|
||||||
|
// The dummy device replies with an m.key.verification.ready
|
||||||
|
returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.sas.v1"]));
|
||||||
|
await waitForVerificationRequestChanged(request);
|
||||||
|
expect(request.phase).toEqual(VerificationPhase.Ready);
|
||||||
|
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(true);
|
||||||
|
|
||||||
|
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||||
|
await jest.advanceTimersByTimeAsync(10);
|
||||||
|
|
||||||
|
// And now Alice starts a SAS verification
|
||||||
|
let sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
|
||||||
|
await request.startVerification("m.sas.v1");
|
||||||
|
let requestBody = await sendToDevicePromise;
|
||||||
|
|
||||||
|
let toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||||
|
expect(toDeviceMessage).toEqual({
|
||||||
|
from_device: aliceClient.deviceId,
|
||||||
|
method: "m.sas.v1",
|
||||||
|
transaction_id: transactionId,
|
||||||
|
hashes: ["sha256"],
|
||||||
|
key_agreement_protocols: expect.arrayContaining(["curve25519-hkdf-sha256"]),
|
||||||
|
message_authentication_codes: expect.arrayContaining(["hkdf-hmac-sha256.v2"]),
|
||||||
|
short_authentication_string: ["decimal", "emoji"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(request.chosenMethod).toEqual("m.sas.v1");
|
||||||
|
|
||||||
|
// There should now be a `verifier`
|
||||||
|
const verifier: Verifier = request.verifier!;
|
||||||
|
expect(verifier).toBeDefined();
|
||||||
|
expect(verifier.getShowSasCallbacks()).toBeNull();
|
||||||
|
const verificationPromise = verifier.verify();
|
||||||
|
|
||||||
|
// The dummy device makes up a curve25519 keypair and uses the hash in an 'm.key.verification.accept'
|
||||||
|
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
|
||||||
|
const olmSAS = new global.Olm.SAS();
|
||||||
|
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(toDeviceMessage);
|
||||||
|
|
||||||
|
sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.key");
|
||||||
|
returnToDeviceMessageFromSync(buildSasAcceptMessage(transactionId, commitmentStr));
|
||||||
|
|
||||||
|
// alice responds with a 'key' ...
|
||||||
|
requestBody = await sendToDevicePromise;
|
||||||
|
|
||||||
|
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||||
|
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||||
|
const aliceDevicePubKeyBase64 = toDeviceMessage.key;
|
||||||
|
olmSAS.set_their_key(aliceDevicePubKeyBase64);
|
||||||
|
|
||||||
|
// ... and the dummy device also sends a 'key'
|
||||||
|
returnToDeviceMessageFromSync(buildSasKeyMessage(transactionId, olmSAS.get_pubkey()));
|
||||||
|
|
||||||
|
// ... and the client is notified to show the emoji
|
||||||
|
const showSas = await new Promise<ShowSasCallbacks>((resolve) => {
|
||||||
|
verifier.once(VerifierEvent.ShowSas, resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
// `getShowSasCallbacks` is an alternative way to get the callbacks
|
||||||
|
expect(verifier.getShowSasCallbacks()).toBe(showSas);
|
||||||
|
expect(verifier.getReciprocateQrCodeCallbacks()).toBeNull();
|
||||||
|
|
||||||
|
// user confirms that the emoji match, and alice sends a 'mac'
|
||||||
|
[requestBody] = await Promise.all([expectSendToDeviceMessage("m.key.verification.mac"), showSas.confirm()]);
|
||||||
|
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||||
|
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||||
|
|
||||||
|
// the dummy device also confirms that the emoji match, and sends a mac
|
||||||
|
returnToDeviceMessageFromSync(
|
||||||
|
buildSasMacMessage(transactionId, olmSAS, TEST_USER_ID, aliceClient.deviceId!),
|
||||||
|
);
|
||||||
|
|
||||||
|
// that should satisfy Alice, who should reply with a 'done'
|
||||||
|
await expectSendToDeviceMessage("m.key.verification.done");
|
||||||
|
|
||||||
|
// the dummy device also confirms done-ness
|
||||||
|
returnToDeviceMessageFromSync(buildDoneMessage(transactionId));
|
||||||
|
|
||||||
|
// ... and the whole thing should be done!
|
||||||
|
await verificationPromise;
|
||||||
|
expect(request.phase).toEqual(VerificationPhase.Done);
|
||||||
|
|
||||||
|
// we're done with the temporary keypair
|
||||||
|
olmSAS.free();
|
||||||
|
});
|
||||||
|
|
||||||
it("Can make a verification request to *all* devices", async () => {
|
it("Can make a verification request to *all* devices", async () => {
|
||||||
aliceClient = await startTestClient();
|
aliceClient = await startTestClient();
|
||||||
// we need an existing cross-signing key for this
|
// we need an existing cross-signing key for this
|
||||||
@@ -584,6 +659,11 @@ function calculateMAC(olmSAS: Olm.SAS, input: string, info: string): string {
|
|||||||
return mac;
|
return mac;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Calculate the sha256 hash of a string, encoding as unpadded base64 */
|
||||||
|
function sha256(commitmentStr: string): string {
|
||||||
|
return encodeUnpaddedBase64(createHash("sha256").update(commitmentStr, "utf8").digest());
|
||||||
|
}
|
||||||
|
|
||||||
function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
|
function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
|
||||||
return Buffer.from(uint8Array).toString("base64").replace(/=+$/g, "");
|
return Buffer.from(uint8Array).toString("base64").replace(/=+$/g, "");
|
||||||
}
|
}
|
||||||
@@ -616,3 +696,64 @@ function buildSasStartMessage(transactionId: string): { type: string; content: o
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** build an m.key.verification.accept to-device message suitable for the SAS flow */
|
||||||
|
function buildSasAcceptMessage(transactionId: string, commitmentStr: string) {
|
||||||
|
return {
|
||||||
|
type: "m.key.verification.accept",
|
||||||
|
content: {
|
||||||
|
transaction_id: transactionId,
|
||||||
|
commitment: sha256(commitmentStr),
|
||||||
|
hash: "sha256",
|
||||||
|
key_agreement_protocol: "curve25519-hkdf-sha256",
|
||||||
|
short_authentication_string: ["decimal", "emoji"],
|
||||||
|
message_authentication_code: "hkdf-hmac-sha256.v2",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** build an m.key.verification.key to-device message suitable for the SAS flow */
|
||||||
|
function buildSasKeyMessage(transactionId: string, key: string): { type: string; content: object } {
|
||||||
|
return {
|
||||||
|
type: "m.key.verification.key",
|
||||||
|
content: {
|
||||||
|
transaction_id: transactionId,
|
||||||
|
key: key,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** build an m.key.verification.mac to-device message suitable for the SAS flow, originating from the dummy device */
|
||||||
|
function buildSasMacMessage(
|
||||||
|
transactionId: string,
|
||||||
|
olmSAS: Olm.SAS,
|
||||||
|
recipientUserId: string,
|
||||||
|
recipientDeviceId: string,
|
||||||
|
): { type: string; content: object } {
|
||||||
|
const macInfoBase = `MATRIX_KEY_VERIFICATION_MAC${TEST_USER_ID}${TEST_DEVICE_ID}${recipientUserId}${recipientDeviceId}${transactionId}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "m.key.verification.mac",
|
||||||
|
content: {
|
||||||
|
keys: calculateMAC(olmSAS, `ed25519:${TEST_DEVICE_ID}`, `${macInfoBase}KEY_IDS`),
|
||||||
|
transaction_id: transactionId,
|
||||||
|
mac: {
|
||||||
|
[`ed25519:${TEST_DEVICE_ID}`]: calculateMAC(
|
||||||
|
olmSAS,
|
||||||
|
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
|
||||||
|
`${macInfoBase}ed25519:${TEST_DEVICE_ID}`,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** build an m.key.verification.done to-device message */
|
||||||
|
function buildDoneMessage(transactionId: string) {
|
||||||
|
return {
|
||||||
|
type: "m.key.verification.done",
|
||||||
|
content: {
|
||||||
|
transaction_id: transactionId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
50
spec/unit/rust-crypto/verification.spec.ts
Normal file
50
spec/unit/rust-crypto/verification.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 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 * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||||
|
import { Mocked } from "jest-mock";
|
||||||
|
|
||||||
|
import { RustVerificationRequest } from "../../../src/rust-crypto/verification";
|
||||||
|
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||||
|
|
||||||
|
describe("VerificationRequest", () => {
|
||||||
|
describe("startVerification", () => {
|
||||||
|
let mockedInner: Mocked<RustSdkCryptoJs.VerificationRequest>;
|
||||||
|
let mockedOutgoingRequestProcessor: Mocked<OutgoingRequestProcessor>;
|
||||||
|
let request: RustVerificationRequest;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockedInner = {
|
||||||
|
registerChangesCallback: jest.fn(),
|
||||||
|
startSas: jest.fn(),
|
||||||
|
} as unknown as Mocked<RustSdkCryptoJs.VerificationRequest>;
|
||||||
|
mockedOutgoingRequestProcessor = {} as Mocked<OutgoingRequestProcessor>;
|
||||||
|
request = new RustVerificationRequest(mockedInner, mockedOutgoingRequestProcessor);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not permit methods other than SAS", async () => {
|
||||||
|
await expect(request.startVerification("m.reciprocate.v1")).rejects.toThrow(
|
||||||
|
"Unsupported verification method",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("raises an error if starting verification does not produce a verifier", async () => {
|
||||||
|
await expect(request.startVerification("m.sas.v1")).rejects.toThrow(
|
||||||
|
"Still no verifier after startSas() call",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -128,9 +128,20 @@ export interface VerificationRequest
|
|||||||
* @param targetDevice - details of where to send the request to.
|
* @param targetDevice - details of where to send the request to.
|
||||||
*
|
*
|
||||||
* @returns The verifier which will do the actual verification.
|
* @returns The verifier which will do the actual verification.
|
||||||
|
*
|
||||||
|
* @deprecated Use {@link VerificationRequest#startVerification} instead.
|
||||||
*/
|
*/
|
||||||
beginKeyVerification(method: string, targetDevice?: { userId?: string; deviceId?: string }): Verifier;
|
beginKeyVerification(method: string, targetDevice?: { userId?: string; deviceId?: string }): Verifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an `m.key.verification.start` event to start verification via a particular method.
|
||||||
|
*
|
||||||
|
* @param method - the name of the verification method to use.
|
||||||
|
*
|
||||||
|
* @returns The verifier which will do the actual verification.
|
||||||
|
*/
|
||||||
|
startVerification(method: string): Promise<Verifier>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The verifier which is doing the actual verification, once the method has been established.
|
* The verifier which is doing the actual verification, once the method has been established.
|
||||||
* Only defined when the `phase` is Started.
|
* Only defined when the `phase` is Started.
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
VerificationRequest as IVerificationRequest,
|
VerificationRequest as IVerificationRequest,
|
||||||
VerificationRequestEvent,
|
VerificationRequestEvent,
|
||||||
VerificationRequestEventHandlerMap,
|
VerificationRequestEventHandlerMap,
|
||||||
|
Verifier,
|
||||||
} from "../../../crypto-api/verification";
|
} from "../../../crypto-api/verification";
|
||||||
|
|
||||||
// backwards-compatibility exports
|
// backwards-compatibility exports
|
||||||
@@ -458,6 +459,13 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
|||||||
return this._verifier!;
|
return this._verifier!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async startVerification(method: string): Promise<Verifier> {
|
||||||
|
const verifier = this.beginKeyVerification(method);
|
||||||
|
// kick off the verification in the background, but *don't* wait for to complete: we need to return the `Verifier`.
|
||||||
|
verifier.verify();
|
||||||
|
return verifier;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* sends the initial .request event.
|
* sends the initial .request event.
|
||||||
* @returns resolves when the event has been sent.
|
* @returns resolves when the event has been sent.
|
||||||
|
|||||||
@@ -236,6 +236,35 @@ export class RustVerificationRequest
|
|||||||
throw new Error("not implemented");
|
throw new Error("not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an `m.key.verification.start` event to start verification via a particular method.
|
||||||
|
*
|
||||||
|
* Implementation of {@link Crypto.VerificationRequest#startVerification}.
|
||||||
|
*
|
||||||
|
* @param method - the name of the verification method to use.
|
||||||
|
*/
|
||||||
|
public async startVerification(method: string): Promise<Verifier> {
|
||||||
|
if (method !== "m.sas.v1") {
|
||||||
|
throw new Error(`Unsupported verification method ${method}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res:
|
||||||
|
| [RustSdkCryptoJs.Sas, RustSdkCryptoJs.RoomMessageRequest | RustSdkCryptoJs.ToDeviceRequest]
|
||||||
|
| undefined = await this.inner.startSas();
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
const [, req] = res;
|
||||||
|
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this should have triggered the onChange callback, and we should now have a verifier
|
||||||
|
if (!this._verifier) {
|
||||||
|
throw new Error("Still no verifier after startSas() call");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._verifier;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The verifier which is doing the actual verification, once the method has been established.
|
* The verifier which is doing the actual verification, once the method has been established.
|
||||||
* Only defined when the `phase` is Started.
|
* Only defined when the `phase` is Started.
|
||||||
|
|||||||
Reference in New Issue
Block a user