1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-09 10:22:46 +03:00
Files
matrix-js-sdk/spec/unit/crypto/verification/sas.spec.ts
2022-12-09 09:38:20 +01:00

554 lines
22 KiB
TypeScript

/*
Copyright 2018-2019 New Vector Ltd
Copyright 2019 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 { makeTestClients } from "./util";
import { MatrixEvent } from "../../../../src/models/event";
import { ISasEvent, SAS, SasEvent } from "../../../../src/crypto/verification/SAS";
import { DeviceInfo, IDevice } from "../../../../src/crypto/deviceinfo";
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
import * as olmlib from "../../../../src/crypto/olmlib";
import { logger } from "../../../../src/logger";
import { resetCrossSigningKeys } from "../crypto-utils";
import { VerificationBase } from "../../../../src/crypto/verification/Base";
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
import { MatrixClient } from "../../../../src";
import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest";
import { TestClient } from "../../../TestClient";
const Olm = global.Olm;
let ALICE_DEVICES: Record<string, IDevice>;
let BOB_DEVICES: Record<string, IDevice>;
describe("SAS verification", function () {
if (!global.Olm) {
logger.warn("Not running device verification unit tests: libolm not present");
return;
}
beforeAll(function () {
return Olm.init();
});
it("should error on an unexpected event", async function () {
//channel, baseApis, userId, deviceId, startEvent, request
const request = {
onVerifierCancelled: function () {},
} as VerificationRequest;
const channel = {
send: function () {
return Promise.resolve();
},
} as unknown as IVerificationChannel;
const mockClient = {} as unknown as MatrixClient;
const event = new MatrixEvent({ type: "test" });
const sas = new SAS(channel, mockClient, "@alice:example.com", "ABCDEFG", event, request);
sas.handleEvent(
new MatrixEvent({
sender: "@alice:example.com",
type: "es.inquisition",
content: {},
}),
);
const spy = jest.fn();
await sas.verify().catch(spy);
expect(spy).toHaveBeenCalled();
// Cancel the SAS for cleanup (we started a verification, so abort)
sas.cancel(new Error("error"));
});
describe("verification", () => {
let alice: TestClient;
let bob: TestClient;
let aliceSasEvent: ISasEvent | null;
let bobSasEvent: ISasEvent | null;
let aliceVerifier: SAS;
let bobPromise: Promise<VerificationBase<any, any>>;
let clearTestClientTimeouts: () => void;
beforeEach(async () => {
[[alice, bob], clearTestClientTimeouts] = await makeTestClients(
[
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{ userId: "@bob:example.com", deviceId: "Dynabook" },
],
{
verificationMethods: [verificationMethods.SAS],
},
);
const aliceDevice = alice.client.crypto!.olmDevice;
const bobDevice = bob.client.crypto!.olmDevice;
ALICE_DEVICES = {
Osborne2: {
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Osborne2": aliceDevice.deviceEd25519Key!,
"curve25519:Osborne2": aliceDevice.deviceCurve25519Key!,
},
verified: DeviceInfo.DeviceVerification.UNVERIFIED,
known: false,
},
};
BOB_DEVICES = {
Dynabook: {
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Dynabook": bobDevice.deviceEd25519Key!,
"curve25519:Dynabook": bobDevice.deviceCurve25519Key!,
},
verified: DeviceInfo.DeviceVerification.UNVERIFIED,
known: false,
},
};
alice.client.crypto!.deviceList.storeDevicesForUser("@bob:example.com", BOB_DEVICES);
alice.client.downloadKeys = () => {
return Promise.resolve({});
};
bob.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", ALICE_DEVICES);
bob.client.downloadKeys = () => {
return Promise.resolve({});
};
aliceSasEvent = null;
bobSasEvent = null;
bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
bob.client.on(CryptoEvent.VerificationRequest, (request) => {
(<SAS>request.verifier!).on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!aliceSasEvent) {
bobSasEvent = e;
} else {
try {
expect(e.sas).toEqual(aliceSasEvent.sas);
e.confirm();
aliceSasEvent.confirm();
} catch (error) {
e.mismatch();
aliceSasEvent.mismatch();
}
}
});
resolve(request.verifier!);
});
});
aliceVerifier = alice.client.beginKeyVerification(
verificationMethods.SAS,
bob.client.getUserId()!,
bob.deviceId!,
) as SAS;
aliceVerifier.on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!bobSasEvent) {
aliceSasEvent = e;
} else {
try {
expect(e.sas).toEqual(bobSasEvent.sas);
e.confirm();
bobSasEvent.confirm();
} catch (error) {
e.mismatch();
bobSasEvent.mismatch();
}
}
});
});
afterEach(async () => {
await Promise.all([alice.stop(), bob.stop()]);
clearTestClientTimeouts();
});
it("should verify a key", async () => {
let macMethod;
let keyAgreement;
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = function (type, map) {
if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
keyAgreement = map[alice.client.getUserId()!][alice.client.deviceId!].key_agreement_protocol;
}
return origSendToDevice(type, map);
};
alice.httpBackend.when("POST", "/keys/query").respond(200, {
failures: {},
device_keys: {
"@bob:example.com": BOB_DEVICES,
},
});
bob.httpBackend.when("POST", "/keys/query").respond(200, {
failures: {},
device_keys: {
"@alice:example.com": ALICE_DEVICES,
},
});
await Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()),
alice.httpBackend.flush(undefined),
bob.httpBackend.flush(undefined),
]);
// make sure that it uses the preferred method
expect(macMethod).toBe("org.matrix.msc3783.hkdf-hmac-sha256");
expect(keyAgreement).toBe("curve25519-hkdf-sha256");
// make sure Alice and Bob verified each other
const bobDevice = await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice?.isVerified()).toBeTruthy();
const aliceDevice = await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice?.isVerified()).toBeTruthy();
});
it("should be able to verify using the old base64", async () => {
// pretend that Alice can only understand the old (incorrect) base64
// encoding, and make sure that she can still verify with Bob
let macMethod;
const aliceOrigSendToDevice = alice.client.sendToDevice.bind(alice.client);
alice.client.sendToDevice = (type, map) => {
if (type === "m.key.verification.start") {
// Note: this modifies not only the message that Bob
// receives, but also the copy of the message that Alice
// has, since it is the same object. If this does not
// happen, the verification will fail due to a hash
// commitment mismatch.
map[bob.client.getUserId()!][bob.client.deviceId!].message_authentication_codes = [
"hkdf-hmac-sha256",
];
}
return aliceOrigSendToDevice(type, map);
};
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = (type, map) => {
if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
}
return bobOrigSendToDevice(type, map);
};
alice.httpBackend.when("POST", "/keys/query").respond(200, {
failures: {},
device_keys: {
"@bob:example.com": BOB_DEVICES,
},
});
bob.httpBackend.when("POST", "/keys/query").respond(200, {
failures: {},
device_keys: {
"@alice:example.com": ALICE_DEVICES,
},
});
await Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()),
alice.httpBackend.flush(undefined),
bob.httpBackend.flush(undefined),
]);
expect(macMethod).toBe("hkdf-hmac-sha256");
const bobDevice = await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice!.isVerified()).toBeTruthy();
const aliceDevice = await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice!.isVerified()).toBeTruthy();
});
it("should be able to verify using the old MAC", async () => {
// pretend that Alice can only understand the old (incorrect) MAC,
// and make sure that she can still verify with Bob
let macMethod;
const aliceOrigSendToDevice = alice.client.sendToDevice.bind(alice.client);
alice.client.sendToDevice = (type, map) => {
if (type === "m.key.verification.start") {
// Note: this modifies not only the message that Bob
// receives, but also the copy of the message that Alice
// has, since it is the same object. If this does not
// happen, the verification will fail due to a hash
// commitment mismatch.
map[bob.client.getUserId()!][bob.client.deviceId!].message_authentication_codes = ["hmac-sha256"];
}
return aliceOrigSendToDevice(type, map);
};
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = (type, map) => {
if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
}
return bobOrigSendToDevice(type, map);
};
alice.httpBackend.when("POST", "/keys/query").respond(200, {
failures: {},
device_keys: {
"@bob:example.com": BOB_DEVICES,
},
});
bob.httpBackend.when("POST", "/keys/query").respond(200, {
failures: {},
device_keys: {
"@alice:example.com": ALICE_DEVICES,
},
});
await Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()),
alice.httpBackend.flush(undefined),
bob.httpBackend.flush(undefined),
]);
expect(macMethod).toBe("hmac-sha256");
const bobDevice = await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice?.isVerified()).toBeTruthy();
const aliceDevice = await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice?.isVerified()).toBeTruthy();
});
it("should verify a cross-signing key", async () => {
alice.httpBackend.when("POST", "/keys/device_signing/upload").respond(200, {});
alice.httpBackend.when("POST", "/keys/signatures/upload").respond(200, {});
alice.httpBackend.flush(undefined, 2);
await resetCrossSigningKeys(alice.client);
bob.httpBackend.when("POST", "/keys/device_signing/upload").respond(200, {});
bob.httpBackend.when("POST", "/keys/signatures/upload").respond(200, {});
bob.httpBackend.flush(undefined, 2);
await resetCrossSigningKeys(bob.client);
bob.client.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: alice.client.crypto!.crossSigningInfo.keys,
crossSigningVerifiedBefore: false,
firstUse: true,
});
const verifyProm = Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => {
bob.httpBackend.when("POST", "/keys/signatures/upload").respond(200, {});
bob.httpBackend.flush(undefined, 1, 2000);
return verifier.verify();
}),
]);
await verifyProm;
const bobDeviceTrust = alice.client.checkDeviceTrust("@bob:example.com", "Dynabook");
expect(bobDeviceTrust.isLocallyVerified()).toBeTruthy();
expect(bobDeviceTrust.isCrossSigningVerified()).toBeFalsy();
const aliceTrust = bob.client.checkUserTrust("@alice:example.com");
expect(aliceTrust.isCrossSigningVerified()).toBeTruthy();
expect(aliceTrust.isTofu()).toBeTruthy();
const aliceDeviceTrust = bob.client.checkDeviceTrust("@alice:example.com", "Osborne2");
expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy();
expect(aliceDeviceTrust.isCrossSigningVerified()).toBeFalsy();
});
});
it("should send a cancellation message on error", async function () {
const [[alice, bob], clearTestClientTimeouts] = await makeTestClients(
[
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{ userId: "@bob:example.com", deviceId: "Dynabook" },
],
{
verificationMethods: [verificationMethods.SAS],
},
);
alice.client.setDeviceVerified = jest.fn();
alice.client.downloadKeys = jest.fn().mockResolvedValue({});
bob.client.setDeviceVerified = jest.fn();
bob.client.downloadKeys = jest.fn().mockResolvedValue({});
const bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
bob.client.on(CryptoEvent.VerificationRequest, (request) => {
(<SAS>request.verifier!).on(SasEvent.ShowSas, (e) => {
e.mismatch();
});
resolve(request.verifier!);
});
});
const aliceVerifier = alice.client.beginKeyVerification(
verificationMethods.SAS,
bob.client.getUserId()!,
bob.client.deviceId!,
);
const aliceSpy = jest.fn();
const bobSpy = jest.fn();
await Promise.all([
aliceVerifier.verify().catch(aliceSpy),
bobPromise.then((verifier) => verifier.verify()).catch(bobSpy),
]);
expect(aliceSpy).toHaveBeenCalled();
expect(bobSpy).toHaveBeenCalled();
expect(alice.client.setDeviceVerified).not.toHaveBeenCalled();
expect(bob.client.setDeviceVerified).not.toHaveBeenCalled();
alice.stop();
bob.stop();
clearTestClientTimeouts();
});
describe("verification in DM", function () {
let alice: TestClient;
let bob: TestClient;
let aliceSasEvent: ISasEvent | null;
let bobSasEvent: ISasEvent | null;
let aliceVerifier: SAS;
let bobPromise: Promise<void>;
let clearTestClientTimeouts: Function;
beforeEach(async function () {
[[alice, bob], clearTestClientTimeouts] = await makeTestClients(
[
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{ userId: "@bob:example.com", deviceId: "Dynabook" },
],
{
verificationMethods: [verificationMethods.SAS],
},
);
alice.client.crypto!.setDeviceVerification = jest.fn();
alice.client.getDeviceEd25519Key = () => {
return "alice+base64+ed25519+key";
};
alice.client.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
keys: {
"ed25519:Dynabook": "bob+base64+ed25519+key",
},
},
"Dynabook",
);
};
alice.client.downloadKeys = () => {
return Promise.resolve({});
};
bob.client.crypto!.setDeviceVerification = jest.fn();
bob.client.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
keys: {
"ed25519:Osborne2": "alice+base64+ed25519+key",
},
},
"Osborne2",
);
};
bob.client.getDeviceEd25519Key = () => {
return "bob+base64+ed25519+key";
};
bob.client.downloadKeys = () => {
return Promise.resolve({});
};
aliceSasEvent = null;
bobSasEvent = null;
bobPromise = new Promise<void>((resolve, reject) => {
bob.client.on(CryptoEvent.VerificationRequest, async (request) => {
const verifier = request.beginKeyVerification(SAS.NAME) as SAS;
verifier.on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!aliceSasEvent) {
bobSasEvent = e;
} else {
try {
expect(e.sas).toEqual(aliceSasEvent.sas);
e.confirm();
aliceSasEvent.confirm();
} catch (error) {
e.mismatch();
aliceSasEvent.mismatch();
}
}
});
await verifier.verify();
resolve();
});
});
const aliceRequest = await alice.client.requestVerificationDM(bob.client.getUserId()!, "!room_id");
await aliceRequest.waitFor((r) => r.started);
aliceVerifier = aliceRequest.verifier! as SAS;
aliceVerifier.on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!bobSasEvent) {
aliceSasEvent = e;
} else {
try {
expect(e.sas).toEqual(bobSasEvent.sas);
e.confirm();
bobSasEvent.confirm();
} catch (error) {
e.mismatch();
bobSasEvent.mismatch();
}
}
});
});
afterEach(async function () {
await Promise.all([alice.stop(), bob.stop()]);
clearTestClientTimeouts();
});
it("should verify a key", async function () {
await Promise.all([aliceVerifier.verify(), bobPromise]);
// make sure Alice and Bob verified each other
expect(alice.client.crypto!.setDeviceVerification).toHaveBeenCalledWith(
bob.client.getUserId(),
bob.client.deviceId,
true,
null,
null,
{ "ed25519:Dynabook": "bob+base64+ed25519+key" },
);
expect(bob.client.crypto!.setDeviceVerification).toHaveBeenCalledWith(
alice.client.getUserId(),
alice.client.deviceId,
true,
null,
null,
{ "ed25519:Osborne2": "alice+base64+ed25519+key" },
);
});
});
});