diff --git a/src/MatrixClient.ts b/src/MatrixClient.ts index 47dc6b72..9a71e7d6 100644 --- a/src/MatrixClient.ts +++ b/src/MatrixClient.ts @@ -44,6 +44,8 @@ import { DMs } from "./DMs"; import { ServerVersions } from "./models/ServerVersions"; import { RoomCreateOptions } from "./models/CreateRoom"; import { PresenceState } from './models/events/PresenceEvent'; +import { IKeyBackupInfo, IKeyBackupInfoRetrieved, IKeyBackupInfoUpdate, IKeyBackupVersion, KeyBackupVersion } from "./models/KeyBackup"; +import { MatrixError } from "./models/MatrixError"; const SYNC_BACKOFF_MIN_MS = 5000; const SYNC_BACKOFF_MAX_MS = 15000; @@ -1962,6 +1964,79 @@ export class MatrixClient extends EventEmitter { }); } + /** + * Get information about the latest room key backup version. + * @returns {Promise} Resolves to the retrieved key backup info, + * or null if there is no existing backup. + */ + public async getKeyBackupVersion(): Promise { + try { + return await this.doRequest("GET", "/_matrix/client/v3/room_keys/version"); + } catch (e) { + if (e instanceof MatrixError && e.errcode === "M_NOT_FOUND") { + return null; + } else { + throw e; + } + } + } + + /** + * Create a new room key backup. + * @param {IKeyBackupInfo} info The properties of the key backup to create. + * @returns {Promise} Resolves to the version id of the new backup. + */ + public async createKeyBackupVersion(info: IKeyBackupInfo): Promise { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + + const data = { + ...info, + signatures: this.crypto.sign(info), + }; + return await this.doRequest("POST", "/_matrix/client/v3/room_keys/version", null, data); + } + + /** + * Update an existing room key backup. + * @param {KeyBackupVersion} version The key backup version to update. + * @param {IKeyBackupInfoUpdate} info The properties of the key backup to be applied. + * @returns {Promise} Resolves when complete. + */ + public async updateKeyBackupVersion(version: KeyBackupVersion, info: IKeyBackupInfoUpdate): Promise { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + + const data = { + ...info, + signatures: this.crypto.sign(info), + }; + await this.doRequest("PUT", `/_matrix/client/v3/room_keys/version/${version}`, null, data); + } + + /** + * Enable backing up of room keys. + * @param {IKeyBackupInfoRetrieved} info The configuration for key backup behaviour, + * as returned by {@link getKeyBackupVersion}. + * @returns {Promise} Resolves when complete. + */ + public async enableKeyBackup(info: IKeyBackupInfoRetrieved): Promise { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + + this.crypto.enableKeyBackup(info); + } + + /** + * Disable backing up of room keys. + */ + public disableKeyBackup(): void { + this.crypto?.disableKeyBackup(); + } + /** * Get relations for a given event. * @param {string} roomId The room ID to for the given event. diff --git a/src/e2ee/CryptoClient.ts b/src/e2ee/CryptoClient.ts index 74ddfb50..42ec98d9 100644 --- a/src/e2ee/CryptoClient.ts +++ b/src/e2ee/CryptoClient.ts @@ -26,6 +26,7 @@ import { EncryptedFile } from "../models/events/MessageEvent"; import { RustSdkCryptoStorageProvider } from "../storage/RustSdkCryptoStorageProvider"; import { RustEngine, SYNC_LOCK_NAME } from "./RustEngine"; import { MembershipEvent } from "../models/events/MembershipEvent"; +import { IKeyBackupInfoRetrieved } from "../models/KeyBackup"; /** * Manages encryption for a MatrixClient. Get an instance from a MatrixClient directly @@ -284,4 +285,21 @@ export class CryptoClient { const decrypted = Attachment.decrypt(encrypted); return Buffer.from(decrypted); } + + /** + * Enable backing up of room keys. + * @param {IKeyBackupInfoRetrieved} info The configuration for key backup behaviour, + * as returned by {@link MatrixClient#getKeyBackupVersion}. + * @returns {Promise} Resolves when complete. + */ + public async enableKeyBackup(info: IKeyBackupInfoRetrieved): Promise { + await this.engine.enableKeyBackup(info); + } + + /** + * Disable backing up of room keys. + */ + public disableKeyBackup(): void { + this.engine.disableKeyBackup(); + } } diff --git a/src/e2ee/RustEngine.ts b/src/e2ee/RustEngine.ts index c9eae9c8..97dfa7cc 100644 --- a/src/e2ee/RustEngine.ts +++ b/src/e2ee/RustEngine.ts @@ -10,13 +10,16 @@ import { KeysUploadRequest, KeysQueryRequest, ToDeviceRequest, + KeysBackupRequest, } from "@matrix-org/matrix-sdk-crypto-nodejs"; import * as AsyncLock from "async-lock"; import { MatrixClient } from "../MatrixClient"; +import { LogService } from "../logging/LogService"; import { ICryptoRoomInformation } from "./ICryptoRoomInformation"; import { EncryptionAlgorithm } from "../models/Crypto"; import { EncryptionEvent } from "../models/events/EncryptionEvent"; +import { IKeyBackupInfoRetrieved, KeyBackupVersion } from "../models/KeyBackup"; /** * @internal @@ -29,6 +32,9 @@ export const SYNC_LOCK_NAME = "sync"; export class RustEngine { public readonly lock = new AsyncLock(); + private keyBackupVersion: KeyBackupVersion|undefined; + // TODO: keyBackupPromise that can be awaited by others + public constructor(public readonly machine: OlmMachine, private client: MatrixClient) { } @@ -59,7 +65,8 @@ export class RustEngine { case RequestType.SignatureUpload: throw new Error("Bindings error: Backup feature not possible"); case RequestType.KeysBackup: - throw new Error("Bindings error: Backup feature not possible"); + await this.processKeysBackupRequest(request); + break; default: throw new Error("Bindings error: Unrecognized request type: " + request.type); } @@ -128,6 +135,17 @@ export class RustEngine { }); } + public async enableKeyBackup(info: IKeyBackupInfoRetrieved) { + LogService.warn("RustEngine", "**STUB** Server-side key backups not yet implemented!"); + this.keyBackupVersion = info.version; + // await this.machine.enableBackupV1(info); + } + + public async disableKeyBackup() { + this.keyBackupVersion = undefined; + // await this.machine.disableBackup(); + } + private async processKeysClaimRequest(request: KeysClaimRequest) { const resp = await this.client.doRequest("POST", "/_matrix/client/v3/keys/claim", null, JSON.parse(request.body)); await this.machine.markRequestAsSent(request.id, request.type, JSON.stringify(resp)); @@ -154,4 +172,15 @@ export class RustEngine { const resp = await this.client.sendToDevices(type, messages); await this.machine.markRequestAsSent(id, RequestType.ToDevice, JSON.stringify(resp)); } + + private async processKeysBackupRequest(request: KeysBackupRequest) { + let resp: Awaited>; + try { + resp = await this.client.doRequest("PUT", "/_matrix/client/v3/room_keys/keys", { version: this.keyBackupVersion }, JSON.parse(request.body)); + } catch (e) { + this.client.emit("crypto.failed_backup", e); + return; + } + await this.machine.markRequestAsSent(request.id, request.type, JSON.stringify(resp)); + } } diff --git a/src/models/KeyBackup.ts b/src/models/KeyBackup.ts new file mode 100644 index 00000000..a8c8eb61 --- /dev/null +++ b/src/models/KeyBackup.ts @@ -0,0 +1,36 @@ +import { Signatures } from "./Crypto"; + +/** + * The kinds of key backup encryption algorithms allowed by the spec. + * @category Models + */ +export enum KeyBackupEncryptionAlgorithm { + MegolmBackupV1Curve25519AesSha2 = "m.megolm_backup.v1.curve25519-aes-sha2", +} + +/** + * Information about a server-side key backup. + */ +export interface IKeyBackupInfo { + algorithm: string | KeyBackupEncryptionAlgorithm; + auth_data: object; +} + +export type KeyBackupVersion = string; + +export interface IKeyBackupVersion { + version: KeyBackupVersion; +} + +export interface IKeyBackupInfoRetrieved extends IKeyBackupInfo, IKeyBackupVersion { + count: number; + etag: string; +} + +export type IKeyBackupInfoUpdate = IKeyBackupInfo & Partial; + +export interface ICurve25519AuthDataUnsigned { + public_key: string; +} + +export type ICurve25519AuthData = ICurve25519AuthDataUnsigned & Signatures;