diff --git a/spec/integ/megolm-integ.spec.ts b/spec/integ/megolm-integ.spec.ts
index fcdfa8694fa..334800333a6 100644
--- a/spec/integ/megolm-integ.spec.ts
+++ b/spec/integ/megolm-integ.spec.ts
@@ -650,6 +650,131 @@ describe("megolm", () => {
]);
});
+ describe("get|setGlobalErrorOnUnknownDevices", () => {
+ it("should raise an error if crypto is disabled", () => {
+ aliceTestClient.client["cryptoBackend"] = undefined;
+ expect(() => aliceTestClient.client.setGlobalErrorOnUnknownDevices(true)).toThrowError(
+ "encryption disabled",
+ );
+ expect(() => aliceTestClient.client.getGlobalErrorOnUnknownDevices()).toThrowError("encryption disabled");
+ });
+
+ it("should permit sending to unknown devices", async () => {
+ expect(aliceTestClient.client.getGlobalErrorOnUnknownDevices()).toBeTruthy();
+
+ aliceTestClient.expectKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
+ await aliceTestClient.start();
+ const p2pSession = await establishOlmSession(aliceTestClient, testOlmAccount);
+
+ aliceTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse(["@bob:xyz"]));
+ await aliceTestClient.flushSync();
+
+ // start out with the device unknown - the send should be rejected.
+ aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, getTestKeysQueryResponse("@bob:xyz"));
+ aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, getTestKeysQueryResponse("@bob:xyz"));
+
+ await Promise.all([
+ aliceTestClient.client.sendTextMessage(ROOM_ID, "test").then(
+ () => {
+ throw new Error("sendTextMessage failed on an unknown device");
+ },
+ (e) => {
+ expect(e.name).toEqual("UnknownDeviceError");
+ },
+ ),
+ aliceTestClient.httpBackend.flushAllExpected(),
+ ]);
+
+ // enable sending to unknown devices, and resend
+ aliceTestClient.client.setGlobalErrorOnUnknownDevices(false);
+ expect(aliceTestClient.client.getGlobalErrorOnUnknownDevices()).toBeFalsy();
+
+ const room = aliceTestClient.client.getRoom(ROOM_ID)!;
+ const pendingMsg = room.getPendingEvents()[0];
+
+ const inboundGroupSessionPromise = expectSendRoomKey(
+ aliceTestClient.httpBackend,
+ "@bob:xyz",
+ testOlmAccount,
+ p2pSession,
+ );
+
+ await Promise.all([
+ aliceTestClient.client.resendEvent(pendingMsg, room),
+ expectSendMegolmMessage(aliceTestClient.httpBackend, inboundGroupSessionPromise),
+ ]);
+ });
+ });
+
+ describe("get|setGlobalBlacklistUnverifiedDevices", () => {
+ it("should raise an error if crypto is disabled", () => {
+ aliceTestClient.client["cryptoBackend"] = undefined;
+ expect(() => aliceTestClient.client.setGlobalBlacklistUnverifiedDevices(true)).toThrowError(
+ "encryption disabled",
+ );
+ expect(() => aliceTestClient.client.getGlobalBlacklistUnverifiedDevices()).toThrowError(
+ "encryption disabled",
+ );
+ });
+
+ it("should disable sending to unverified devices", async () => {
+ aliceTestClient.expectKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
+ await aliceTestClient.start();
+ const p2pSession = await establishOlmSession(aliceTestClient, testOlmAccount);
+
+ // tell alice we share a room with bob
+ aliceTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse(["@bob:xyz"]));
+ await aliceTestClient.flushSync();
+
+ logger.log("Forcing alice to download our device keys");
+ aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, getTestKeysQueryResponse("@bob:xyz"));
+ aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, getTestKeysQueryResponse("@bob:xyz"));
+
+ await Promise.all([
+ aliceTestClient.client.downloadKeys(["@bob:xyz"]),
+ aliceTestClient.httpBackend.flush("/keys/query", 2),
+ ]);
+
+ logger.log("Telling alice to block messages to unverified devices");
+ expect(aliceTestClient.client.getGlobalBlacklistUnverifiedDevices()).toBeFalsy();
+ aliceTestClient.client.setGlobalBlacklistUnverifiedDevices(true);
+ expect(aliceTestClient.client.getGlobalBlacklistUnverifiedDevices()).toBeTruthy();
+
+ logger.log("Telling alice to send a megolm message");
+ aliceTestClient.httpBackend.when("PUT", "/send/").respond(200, { event_id: "$event_id" });
+ aliceTestClient.httpBackend.when("PUT", "/sendToDevice/m.room_key.withheld/").respond(200, {});
+
+ await Promise.all([
+ aliceTestClient.client.sendTextMessage(ROOM_ID, "test"),
+ aliceTestClient.httpBackend.flushAllExpected({ timeout: 1000 }),
+ ]);
+
+ // Now, let's mark the device as verified, and check that keys are sent to it.
+
+ logger.log("Marking the device as verified");
+ // XXX: this is an integration test; we really ought to do this via the cross-signing dance
+ const d = aliceTestClient.client.crypto!.deviceList.getStoredDevice("@bob:xyz", "DEVICE_ID")!;
+ d.verified = DeviceInfo.DeviceVerification.VERIFIED;
+ aliceTestClient.client.crypto?.deviceList.storeDevicesForUser("@bob:xyz", { DEVICE_ID: d });
+
+ const inboundGroupSessionPromise = expectSendRoomKey(
+ aliceTestClient.httpBackend,
+ "@bob:xyz",
+ testOlmAccount,
+ p2pSession,
+ );
+
+ logger.log("Asking alice to re-send");
+ await Promise.all([
+ expectSendMegolmMessage(aliceTestClient.httpBackend, inboundGroupSessionPromise).then((decrypted) => {
+ expect(decrypted.type).toEqual("m.room.message");
+ expect(decrypted.content!.body).toEqual("test");
+ }),
+ aliceTestClient.client.sendTextMessage(ROOM_ID, "test"),
+ ]);
+ });
+ });
+
it("We should start a new megolm session when a device is blocked", async () => {
aliceTestClient.expectKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await aliceTestClient.start();
diff --git a/spec/unit/crypto/cross-signing.spec.ts b/spec/unit/crypto/cross-signing.spec.ts
index 6f15bdd09ab..eddaf43ba4a 100644
--- a/spec/unit/crypto/cross-signing.spec.ts
+++ b/spec/unit/crypto/cross-signing.spec.ts
@@ -1147,7 +1147,7 @@ describe("userHasCrossSigningKeys", function () {
});
it("throws an error if crypto is disabled", () => {
- aliceClient.crypto = undefined;
+ aliceClient["cryptoBackend"] = undefined;
expect(() => aliceClient.userHasCrossSigningKeys()).toThrowError("encryption disabled");
});
});
diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts
index 8fd3c22b936..6c96b838ee3 100644
--- a/spec/unit/room.spec.ts
+++ b/spec/unit/room.spec.ts
@@ -3130,7 +3130,7 @@ describe("Room", function () {
it("should load pending events from from the store and decrypt if needed", async () => {
const client = new TestClient(userA).client;
- client.crypto = {
+ client.crypto = client["cryptoBackend"] = {
decryptEvent: jest.fn().mockResolvedValue({ clearEvent: { body: "enc" } }),
} as unknown as Crypto;
client.store.getPendingEvents = jest.fn(async (roomId) => [
diff --git a/src/@types/crypto.ts b/src/@types/crypto.ts
index 3c46d993910..91e4d161c7b 100644
--- a/src/@types/crypto.ts
+++ b/src/@types/crypto.ts
@@ -14,7 +14,33 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import type { IClearEvent } from "../models/event";
+
export type OlmGroupSessionExtraData = {
untrusted?: boolean;
sharedHistory?: boolean;
};
+
+/**
+ * The result of a (successful) call to {@link Crypto.decryptEvent}
+ */
+export interface IEventDecryptionResult {
+ /**
+ * The plaintext payload for the event (typically containing type and content fields).
+ */
+ clearEvent: IClearEvent;
+ /**
+ * List of curve25519 keys involved in telling us about the senderCurve25519Key and claimedEd25519Key.
+ * See {@link MatrixEvent#getForwardingCurve25519KeyChain}.
+ */
+ forwardingCurve25519KeyChain?: string[];
+ /**
+ * Key owned by the sender of this event. See {@link MatrixEvent#getSenderKey}.
+ */
+ senderCurve25519Key?: string;
+ /**
+ * ed25519 key claimed by the sender of this event. See {@link MatrixEvent#getClaimedEd25519Key}.
+ */
+ claimedEd25519Key?: string;
+ untrusted?: boolean;
+}
diff --git a/src/client.ts b/src/client.ts
index 66b16903039..83c4ac2cd12 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -210,6 +210,7 @@ import { UIARequest, UIAResponse } from "./@types/uia";
import { LocalNotificationSettings } from "./@types/local_notifications";
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync";
import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature";
+import { CryptoBackend } from "./common-crypto/CryptoBackend";
export type Store = IStore;
@@ -1147,7 +1148,8 @@ export class MatrixClient extends TypedEventEmitter } = {};
public identityServer?: IIdentityServerProvider;
public http: MatrixHttpApi; // XXX: Intended private, used in code.
- public crypto?: Crypto; // XXX: Intended private, used in code.
+ public crypto?: Crypto; // libolm crypto implementation. XXX: Intended private, used in code. Being replaced by cryptoBackend
+ private cryptoBackend?: CryptoBackend; // one of crypto or rustCrypto
public cryptoCallbacks: ICryptoCallbacks; // XXX: Intended private, used in code.
public callEventHandler?: CallEventHandler; // XXX: Intended private, used in code.
public groupCallEventHandler?: GroupCallEventHandler;
@@ -1455,7 +1457,7 @@ export class MatrixClient extends TypedEventEmitter[0]);
- this.crypto = crypto;
+ this.cryptoBackend = this.crypto = crypto;
// upload our keys in the background
this.crypto.uploadDeviceKeys().catch((e) => {
@@ -2054,7 +2056,7 @@ export class MatrixClient extends TypedEventEmitter {
- if (!this.crypto) {
+ if (!this.cryptoBackend) {
throw new Error("End-to-end encryption disabled");
}
- return this.crypto.userHasCrossSigningKeys();
+ return this.cryptoBackend.userHasCrossSigningKeys();
}
/**
@@ -7162,7 +7164,7 @@ export class MatrixClient extends TypedEventEmitter {
if (event.shouldAttemptDecryption() && this.isCryptoEnabled()) {
- event.attemptDecryption(this.crypto!, options);
+ event.attemptDecryption(this.cryptoBackend!, options);
}
if (event.isBeingDecrypted()) {
diff --git a/src/common-crypto/CryptoBackend.ts b/src/common-crypto/CryptoBackend.ts
new file mode 100644
index 00000000000..77748cb5050
--- /dev/null
+++ b/src/common-crypto/CryptoBackend.ts
@@ -0,0 +1,63 @@
+/*
+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 type { IEventDecryptionResult } from "../@types/crypto";
+import { MatrixEvent } from "../models/event";
+
+/**
+ * Common interface for the crypto implementations
+ */
+export interface CryptoBackend {
+ /**
+ * Global override for whether the client should ever send encrypted
+ * messages to unverified devices. This provides the default for rooms which
+ * do not specify a value.
+ *
+ * If true, all unverified devices will be blacklisted by default
+ */
+ globalBlacklistUnverifiedDevices: boolean;
+
+ /**
+ * Whether sendMessage in a room with unknown and unverified devices
+ * should throw an error and not send the message. This has 'Global' for
+ * symmetry with setGlobalBlacklistUnverifiedDevices but there is currently
+ * no room-level equivalent for this setting.
+ */
+ globalErrorOnUnknownDevices: boolean;
+
+ /**
+ * Shut down any background processes related to crypto
+ */
+ stop(): void;
+
+ /**
+ * Checks if the user has previously published cross-signing keys
+ *
+ * This means downloading the devicelist for the user and checking if the list includes
+ * the cross-signing pseudo-device.
+
+ * @returns true if the user has previously published cross-signing keys
+ */
+ userHasCrossSigningKeys(): Promise;
+
+ /**
+ * Decrypt a received event
+ *
+ * @returns a promise which resolves once we have finished decrypting.
+ * Rejects with an error if there is a problem decrypting the event.
+ */
+ decryptEvent(event: MatrixEvent): Promise;
+}
diff --git a/src/common-crypto/README.md b/src/common-crypto/README.md
new file mode 100644
index 00000000000..7af3298af48
--- /dev/null
+++ b/src/common-crypto/README.md
@@ -0,0 +1,4 @@
+This directory contains functionality which is common to both the legacy (libolm-based) crypto implementation,
+and the new rust-based implementation.
+
+It is an internal module, and is _not_ directly exposed to applications.
diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts
index e8816569339..ef04e8f4e0b 100644
--- a/src/crypto/algorithms/megolm.ts
+++ b/src/crypto/algorithms/megolm.ts
@@ -20,6 +20,7 @@ limitations under the License.
import { v4 as uuidv4 } from "uuid";
+import type { IEventDecryptionResult } from "../../@types/crypto";
import { logger } from "../../logger";
import * as olmlib from "../olmlib";
import {
@@ -38,13 +39,7 @@ import { IOlmSessionResult } from "../olmlib";
import { DeviceInfoMap } from "../DeviceList";
import { IContent, MatrixEvent } from "../../models/event";
import { EventType, MsgType, ToDeviceMessageId } from "../../@types/event";
-import {
- IMegolmEncryptedContent,
- IEventDecryptionResult,
- IMegolmSessionData,
- IncomingRoomKeyRequest,
- IEncryptedContent,
-} from "../index";
+import { IMegolmEncryptedContent, IMegolmSessionData, IncomingRoomKeyRequest, IEncryptedContent } from "../index";
import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager";
import { OlmGroupSessionExtraData } from "../../@types/crypto";
import { MatrixError } from "../../http-api";
diff --git a/src/crypto/algorithms/olm.ts b/src/crypto/algorithms/olm.ts
index 90e724d3804..1a795545916 100644
--- a/src/crypto/algorithms/olm.ts
+++ b/src/crypto/algorithms/olm.ts
@@ -18,13 +18,14 @@ limitations under the License.
* Defines m.olm encryption/decryption
*/
+import type { IEventDecryptionResult } from "../../@types/crypto";
import { logger } from "../../logger";
import * as olmlib from "../olmlib";
import { DeviceInfo } from "../deviceinfo";
import { DecryptionAlgorithm, DecryptionError, EncryptionAlgorithm, registerAlgorithm } from "./base";
import { Room } from "../../models/room";
import { IContent, MatrixEvent } from "../../models/event";
-import { IEncryptedContent, IEventDecryptionResult, IOlmEncryptedContent } from "../index";
+import { IEncryptedContent, IOlmEncryptedContent } from "../index";
import { IInboundSession } from "../OlmDevice";
const DeviceVerification = DeviceInfo.DeviceVerification;
diff --git a/src/crypto/index.ts b/src/crypto/index.ts
index 391e6975e7d..9349550130d 100644
--- a/src/crypto/index.ts
+++ b/src/crypto/index.ts
@@ -20,6 +20,7 @@ limitations under the License.
import anotherjson from "another-json";
import { v4 as uuidv4 } from "uuid";
+import type { IEventDecryptionResult } from "../@types/crypto";
import type { PkDecryption, PkSigning } from "@matrix-org/olm";
import { EventType, ToDeviceMessageId } from "../@types/event";
import { TypedReEmitter } from "../ReEmitter";
@@ -67,7 +68,7 @@ import { BackupManager } from "./backup";
import { IStore } from "../store";
import { Room, RoomEvent } from "../models/room";
import { RoomMember, RoomMemberEvent } from "../models/room-member";
-import { EventStatus, IClearEvent, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event";
+import { EventStatus, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event";
import { ToDeviceBatch } from "../models/ToDeviceMessage";
import {
ClientEvent,
@@ -87,6 +88,7 @@ import { IContent } from "../models/event";
import { ISyncResponse } from "../sync-accumulator";
import { ISignatures } from "../@types/signed";
import { IMessage } from "./algorithms/olm";
+import { CryptoBackend } from "../common-crypto/CryptoBackend";
const DeviceVerification = DeviceInfo.DeviceVerification;
@@ -218,30 +220,6 @@ interface ISignableObject {
unsigned?: object;
}
-/**
- * The result of a (successful) call to decryptEvent.
- */
-export interface IEventDecryptionResult {
- /**
- * The plaintext payload for the event (typically containing type and content fields).
- */
- clearEvent: IClearEvent;
- /**
- * List of curve25519 keys involved in telling us about the senderCurve25519Key and claimedEd25519Key.
- * See {@link MatrixEvent#getForwardingCurve25519KeyChain}.
- */
- forwardingCurve25519KeyChain?: string[];
- /**
- * Key owned by the sender of this event. See {@link MatrixEvent#getSenderKey}.
- */
- senderCurve25519Key?: string;
- /**
- * ed25519 key claimed by the sender of this event. See {@link MatrixEvent#getClaimedEd25519Key}.
- */
- claimedEd25519Key?: string;
- untrusted?: boolean;
-}
-
export interface IRequestsMap {
getRequest(event: MatrixEvent): VerificationRequest | undefined;
getRequestByChannel(channel: IVerificationChannel): VerificationRequest | undefined;
@@ -380,7 +358,7 @@ export type CryptoEventHandlerMap = {
[CryptoEvent.UserCrossSigningUpdated]: (userId: string) => void;
};
-export class Crypto extends TypedEventEmitter {
+export class Crypto extends TypedEventEmitter implements CryptoBackend {
/**
* @returns The version of Olm.
*/
@@ -3915,3 +3893,6 @@ class IncomingRoomKeyRequestCancellation {
this.requestId = content.request_id;
}
}
+
+// IEventDecryptionResult is re-exported for backwards compatibility, in case any applications are referencing it.
+export type { IEventDecryptionResult } from "../@types/crypto";
diff --git a/src/models/event.ts b/src/models/event.ts
index c4910e752c1..1f1c3cfa366 100644
--- a/src/models/event.ts
+++ b/src/models/event.ts
@@ -21,10 +21,11 @@ limitations under the License.
import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk";
+import type { IEventDecryptionResult } from "../@types/crypto";
import { logger } from "../logger";
import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
import { EVENT_VISIBILITY_CHANGE_TYPE, EventType, MsgType, RelationType } from "../@types/event";
-import { Crypto, IEventDecryptionResult } from "../crypto";
+import { Crypto } from "../crypto";
import { deepSortedObjectEntries, internaliseString } from "../utils";
import { RoomMember } from "./room-member";
import { Thread, ThreadEvent, EventHandlerMap as ThreadEventHandlerMap, THREAD_RELATION_TYPE } from "./thread";
@@ -34,6 +35,7 @@ import { MatrixError } from "../http-api";
import { TypedEventEmitter } from "./typed-event-emitter";
import { EventStatus } from "./event-status";
import { DecryptionError } from "../crypto/algorithms";
+import { CryptoBackend } from "../common-crypto/CryptoBackend";
export { EventStatus } from "./event-status";
@@ -725,7 +727,7 @@ export class MatrixEvent extends TypedEventEmitter {
+ public async attemptDecryption(crypto: CryptoBackend, options: IDecryptOptions = {}): Promise {
// start with a couple of sanity checks.
if (!this.isEncrypted()) {
throw new Error("Attempt to decrypt event which isn't encrypted");
@@ -803,7 +805,7 @@ export class MatrixEvent extends TypedEventEmitter {
+ private async decryptionLoop(crypto: CryptoBackend, options: IDecryptOptions = {}): Promise {
// make sure that this method never runs completely synchronously.
// (doing so would mean that we would clear decryptionPromise *before*
// it is set in attemptDecryption - and hence end up with a stuck