Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rust backup support ground work #3548

Closed
wants to merge 14 commits into from
Closed
35 changes: 22 additions & 13 deletions spec/integ/crypto/megolm-backup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ import { logger } from "../../../src/logger";
import { decodeRecoveryKey } from "../../../src/crypto/recoverykey";
import { IKeyBackupInfo, IKeyBackupSession } from "../../../src/crypto/keybackup";
import { createClient, ICreateClientOpts, IEvent, MatrixClient } from "../../../src";
import { MatrixEventEvent } from "../../../src/models/event";
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import { SyncResponder } from "../../test-utils/SyncResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
import { syncPromise } from "../../test-utils/test-utils";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { MatrixEventEvent } from "../../../src/models/event";

const ROOM_ID = "!ROOM:ID";

Expand Down Expand Up @@ -87,7 +87,10 @@ const RECOVERY_KEY = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4
const TEST_USER_ID = "@alice:localhost";
const TEST_DEVICE_ID = "xzcvb";

describe("megolm key backups", function () {
describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backend: string, initCrypto: InitCrypto) => {
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
// Rust backend. Once we have full support in the rust sdk, it will go away.
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
let aliceClient: MatrixClient;
/** an object which intercepts `/sync` requests on the test homeserver */
let syncResponder: SyncResponder;
Expand Down Expand Up @@ -130,12 +133,12 @@ describe("megolm key backups", function () {
deviceId: TEST_DEVICE_ID,
...opts,
});
await client.initCrypto();
await initCrypto(client);

return client;
}

it("Alice checks key backups when receiving a message she can't decrypt", async function () {
oldBackendOnly("Alice checks key backups when receiving a message she can't decrypt", async function () {
const syncResponse = {
next_batch: 1,
rooms: {
Expand All @@ -154,15 +157,20 @@ describe("megolm key backups", function () {
// mock for the outgoing key requests that will be sent
fetchMock.put("express:/_matrix/client/r0/sendToDevice/m.room_key_request/:txid", {});

// We'll need to add a signature to the backup data, so take a copy to avoid mutating global state.
const backupData = JSON.parse(JSON.stringify(CURVE25519_BACKUP_INFO));
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData);
fetchMock.get("express:/_matrix/client/v3/room_keys/version", CURVE25519_BACKUP_INFO);

aliceClient = await initTestClient();
await aliceClient.crypto!.signObject(backupData.auth_data);
await aliceClient.crypto!.storeSessionBackupPrivateKey(decodeRecoveryKey(RECOVERY_KEY));
await aliceClient.crypto!.backupManager!.checkAndStart();

// we need the backup to be trusted for the test to work
const backupDataToSign = JSON.parse(JSON.stringify(CURVE25519_BACKUP_INFO));

await aliceClient.getCrypto()!.signObject(backupDataToSign.auth_data);
fetchMock.get("express:/_matrix/client/v3/room_keys/version", backupDataToSign, {
overwriteRoutes: true,
});

await aliceClient.getCrypto()!.storeSessionBackupPrivateKey(decodeRecoveryKey(RECOVERY_KEY));
await aliceClient.getCrypto()!.getBackupManager()!.checkAndStart();
// start after saving the private key
await aliceClient.startClient();

Expand All @@ -172,6 +180,7 @@ describe("megolm key backups", function () {
const room = aliceClient.getRoom(ROOM_ID)!;

const event = room.getLiveTimeline().getEvents()[0];

await new Promise((resolve, reject) => {
event.once(MatrixEventEvent.Decrypted, (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
Expand Down
5 changes: 5 additions & 0 deletions src/@types/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,8 @@ export interface IOneTimeKey {
fallback?: boolean;
signatures?: ISignatures;
}

export interface ISignableObject {
signatures?: ISignatures;
unsigned?: object;
}
4 changes: 3 additions & 1 deletion src/common-crypto/CryptoBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ limitations under the License.
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator";
import { MatrixEvent } from "../models/event";
import { Room } from "../models/room";
import { CryptoApi } from "../crypto-api";
import { CryptoApi, SecureKeyBackup } from "../crypto-api";
import { CrossSigningInfo, UserTrustLevel } from "../crypto/CrossSigning";
import { IEncryptedEventInfo } from "../crypto/api";
import { IEventDecryptionResult } from "../@types/crypto";
Expand Down Expand Up @@ -99,6 +99,8 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
* @deprecated Unneeded for the new crypto
*/
checkOwnCrossSigningTrust(opts?: CheckOwnCrossSigningTrustOpts): Promise<void>;

getBackupManager(): SecureKeyBackup;
}

/** The methods which crypto implementations should expose to the Sync api
Expand Down
26 changes: 24 additions & 2 deletions src/crypto-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import type { IMegolmSessionData } from "./@types/crypto";
import type { IMegolmSessionData, ISignableObject } from "./@types/crypto";
import { Room } from "./models/room";
import { DeviceMap } from "./models/device";
import { UIAuthCallback } from "./interactive-auth";
import { AddSecretStorageKeyOpts, SecretStorageCallbacks, SecretStorageKeyDescription } from "./secret-storage";
import { VerificationRequest } from "./crypto-api/verification";
import { KeyBackupInfo } from "./crypto-api/keybackup";
import { KeyBackupInfo, SecureKeyBackup } from "./crypto-api/keybackup";

/**
* Public interface to the cryptography parts of the js-sdk
Expand Down Expand Up @@ -284,6 +284,28 @@ export interface CryptoApi {
* @returns a VerificationRequest when the request has been sent to the other party.
*/
requestDeviceVerification(userId: string, deviceId: string): Promise<VerificationRequest>;

/**
* Fetches the backup private key, if cached
* @returns the key, if any, or null
*/
getSessionBackupPrivateKey(): Promise<Uint8Array | null>;

/**
* Stores the session backup key to the cache
* @param key - the private key
* @returns a promise so you can catch failures
*/
storeSessionBackupPrivateKey(key: ArrayLike<number>): Promise<void>;

/**
* sign the given object with our ed25519 key
*
* @param obj - Object to which we will add a 'signatures' property
*/
signObject<T extends ISignableObject & object>(obj: T): Promise<void>;
Comment on lines +301 to +306
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you think we actually need this? I note that the only place it is called (MatrixClient.createKeyBackupVersion) has this comment:

This can probably go away very soon in
favour of just signing with the cross-singing master key.

is "very soon" less than 3.5 years?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mm, not sure about the comment. What we will stop doing is trusting a backup if it's signed by a device we trust. But I think signing with our own device is a way to mark as trusted.


getBackupManager(): SecureKeyBackup;
}

/**
Expand Down
70 changes: 70 additions & 0 deletions src/crypto-api/keybackup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ limitations under the License.
*/

import { ISigned } from "../@types/signed";
import { DeviceTrustLevel } from "../crypto/CrossSigning";
import { DeviceInfo } from "../crypto/deviceinfo";
import { IKeyBackupInfo } from "../crypto/keybackup";

export interface Curve25519AuthData {
public_key: string;
Expand All @@ -40,3 +43,70 @@ export interface KeyBackupInfo {
etag?: string;
version?: string; // number contained within
}

/**
* Status of the active key backup.
*/
export interface KeyBackupStatus {
/** The backup version */
version: string;
/** True if the client is backing up keys to this backup */
enabled: boolean;
}

/**
* Detailed signature information of a backup.
* This can be used to display what devices/identities are trusting a backup.
*/
export type SigInfo = {
deviceId: string;
valid?: boolean | null; // true: valid, false: invalid, null: cannot attempt validation
device?: DeviceInfo | null;
crossSigningId?: boolean;
deviceTrust?: DeviceTrustLevel;
};

/**
* Backup trust information.
* Client can upload and download from backup if `usable` is true.
*/
export type TrustInfo = {
usable: boolean; // is the backup trusted, true iff there is a sig that is valid & from a trusted device
sigs: SigInfo[];
// eslint-disable-next-line camelcase
trusted_locally?: boolean; // true if the private key is known. Notice that this is not enough to use the backup.
};

/**
* Active key backup info, if any.
* Returned as a result of `checkAndStart()`.
*/
export interface IKeyBackupCheck {
backupInfo?: IKeyBackupInfo;
trustInfo: TrustInfo;
}

/**
* Server side keys backup management.
* Devices may upload encrypted copies of keys to the server.
* When a device tries to read a message that it does not have keys for, it may request the key from the server and decrypt it.
*/
export interface SecureKeyBackup {
/**
* Gets the status of the current active key backup if any.
*/
getKeyBackupStatus(): Promise<KeyBackupStatus | null>;

/**
* Stop the SecureKeyBackup manager from backing up keys and allow a clean shutdown.
*/
stop(): void;

/**
* Check the server for an active key backup and
* if one is present and has a valid signature from
* one of the user's verified devices, start backing up
* to it.
*/
checkAndStart(): Promise<IKeyBackupCheck | null>;
}
41 changes: 19 additions & 22 deletions src/crypto/backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ import type { IMegolmSessionData } from "../@types/crypto";
import { MatrixClient } from "../client";
import { logger } from "../logger";
import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
import { DeviceInfo } from "./deviceinfo";
import { DeviceTrustLevel } from "./CrossSigning";
import { keyFromPassphrase } from "./key_passphrase";
import { safeSet, sleep } from "../utils";
import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
Expand All @@ -36,6 +34,7 @@ import {
IKeyBackupInfo,
IKeyBackupSession,
} from "./keybackup";
import { IKeyBackupCheck, KeyBackupStatus, SecureKeyBackup, SigInfo, TrustInfo } from "../crypto-api";
import { UnstableValue } from "../NamespacedValue";
import { CryptoEvent } from "./index";
import { crypto } from "./crypto";
Expand All @@ -46,25 +45,8 @@ const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms

type AuthData = IKeyBackupInfo["auth_data"];

type SigInfo = {
deviceId: string;
valid?: boolean | null; // true: valid, false: invalid, null: cannot attempt validation
device?: DeviceInfo | null;
crossSigningId?: boolean;
deviceTrust?: DeviceTrustLevel;
};

export type TrustInfo = {
usable: boolean; // is the backup trusted, true iff there is a sig that is valid & from a trusted device
sigs: SigInfo[];
// eslint-disable-next-line camelcase
trusted_locally?: boolean;
};

export interface IKeyBackupCheck {
backupInfo?: IKeyBackupInfo;
trustInfo: TrustInfo;
}
// re-export for backward compatibility
export type { TrustInfo, SigInfo, IKeyBackupCheck } from "../crypto-api/keybackup";

/* eslint-disable camelcase */
export interface IPreparedKeyBackupVersion {
Expand Down Expand Up @@ -112,7 +94,7 @@ export interface IKeyBackup {
/**
* Manages the key backup.
*/
export class BackupManager {
export class BackupManager implements SecureKeyBackup {
private algorithm: BackupAlgorithm | undefined;
public backupInfo: IKeyBackupInfo | undefined; // The info dict from /room_keys/version
public checkedForBackup: boolean; // Have we checked the server for a backup we can use?
Expand Down Expand Up @@ -192,13 +174,28 @@ export class BackupManager {
this.baseApis.emit(CryptoEvent.KeyBackupStatus, false);
}

/* @deprecated use `getKeyBackupStatus` instead */
public getKeyBackupEnabled(): boolean | null {
if (!this.checkedForBackup) {
return null;
}
return Boolean(this.algorithm);
}

public async getKeyBackupStatus(): Promise<KeyBackupStatus | null> {
if (!this.checkedForBackup) {
return null;
}
if (this.algorithm && this.version) {
return {
version: this.version,
enabled: true,
};
} else {
return null;
}
}

public async prepareKeyBackupVersion(
key?: string | Uint8Array | null,
algorithm?: string | undefined,
Expand Down
25 changes: 17 additions & 8 deletions src/crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ limitations under the License.
import anotherjson from "another-json";
import { v4 as uuidv4 } from "uuid";

import type { IDeviceKeys, IEventDecryptionResult, IMegolmSessionData, IOneTimeKey } from "../@types/crypto";
import type {
IDeviceKeys,
IEventDecryptionResult,
IMegolmSessionData,
IOneTimeKey,
ISignableObject,
} from "../@types/crypto";
import type { PkDecryption, PkSigning } from "@matrix-org/olm";
import { EventType, ToDeviceMessageId } from "../@types/event";
import { TypedReEmitter } from "../ReEmitter";
Expand Down Expand Up @@ -71,7 +77,6 @@ import { CryptoStore } from "./store/base";
import { IVerificationChannel } from "./verification/request/Channel";
import { TypedEventEmitter } from "../models/typed-event-emitter";
import { IDeviceLists, ISyncResponse, IToDeviceEvent } from "../sync-accumulator";
import { ISignatures } from "../@types/signed";
import { IMessage } from "./algorithms/olm";
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
import { RoomState, RoomStateEvent } from "../models/room-state";
Expand All @@ -92,6 +97,7 @@ import {
DeviceVerificationStatus,
ImportRoomKeysOpts,
VerificationRequest as CryptoApiVerificationRequest,
SecureKeyBackup,
} from "../crypto-api";
import { Device, DeviceMap } from "../models/device";
import { deviceInfoToDevice } from "./device-converter";
Expand All @@ -102,6 +108,9 @@ export type {
CryptoCallbacks as ICryptoCallbacks,
} from "../crypto-api";

// re export for backward compatibility
export type { ISignableObject } from "../@types/crypto";

const DeviceVerification = DeviceInfo.DeviceVerification;

const defaultVerificationMethods = {
Expand Down Expand Up @@ -182,11 +191,6 @@ export interface IRoomKeyRequestRecipient {
deviceId: string;
}

interface ISignableObject {
signatures?: ISignatures;
unsigned?: object;
}

export interface IRequestsMap {
getRequest(event: MatrixEvent): VerificationRequest | undefined;
getRequestByChannel(channel: IVerificationChannel): VerificationRequest | undefined;
Expand Down Expand Up @@ -350,6 +354,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return OlmDevice.getOlmVersion();
}

/** @deprecated use `getBackupManager` */
public readonly backupManager: BackupManager;
public readonly crossSigningInfo: CrossSigningInfo;
public readonly olmDevice: OlmDevice;
Expand Down Expand Up @@ -539,6 +544,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
}

public getBackupManager(): SecureKeyBackup {
return this.backupManager;
}

/**
* Initialise the crypto module so that it is ready for use
*
Expand Down Expand Up @@ -1838,7 +1847,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
this.outgoingRoomKeyRequestManager.stop();
this.deviceList.stop();
this.dehydrationManager.stop();
this.backupManager.stop();
this.getBackupManager().stop();
}

/**
Expand Down
Loading