diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts index 7de8fa0cf..b27f4102b 100644 --- a/spec/integ/crypto/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -68,7 +68,7 @@ import { mockSetupMegolmBackupRequests, } from "../../test-utils/mockEndpoints"; import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage"; -import { CryptoCallbacks, KeyBackupInfo } from "../../../src/crypto-api"; +import { CrossSigningKey, CryptoCallbacks, KeyBackupInfo } from "../../../src/crypto-api"; import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder"; afterEach(() => { @@ -2247,7 +2247,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, } /** - * Add all mocks needed to set up cross-signing, key backup, 4S and then + * Add all mocks needed to setup cross-signing, key backup, 4S and then * configure the account to have recovery. * * @param backupVersion - The version of the created backup @@ -2295,7 +2295,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, await bootstrapPromise; // Finally ensure backup is working await aliceClient.getCrypto()!.checkKeyBackupAndEnable(); - await backupStatusUpdate; } @@ -2346,7 +2345,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, }, ); - newBackendOnly("should create a new key", async () => { + it("should create a new key", async () => { const bootstrapPromise = aliceClient .getCrypto()! .bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey }); @@ -2389,46 +2388,43 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, }, ); - newBackendOnly( - "should create a new key if setupNewSecretStorage is at true even if an AES key is already in the secret storage", - async () => { - let bootstrapPromise = aliceClient - .getCrypto()! - .bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey }); + it("should create a new key if setupNewSecretStorage is at true even if an AES key is already in the secret storage", async () => { + let bootstrapPromise = aliceClient + .getCrypto()! + .bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey }); - // Wait for the key to be uploaded in the account data - let secretStorageKey = await awaitSecretStorageKeyStoredInAccountData(); + // Wait for the key to be uploaded in the account data + let secretStorageKey = await awaitSecretStorageKeyStoredInAccountData(); - // Return the newly created key in the sync response - sendSyncResponse(secretStorageKey); + // Return the newly created key in the sync response + sendSyncResponse(secretStorageKey); - // Wait for bootstrapSecretStorage to finished - await bootstrapPromise; + // Wait for bootstrapSecretStorage to finished + await bootstrapPromise; - // Call again bootstrapSecretStorage - bootstrapPromise = aliceClient - .getCrypto()! - .bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey }); + // Call again bootstrapSecretStorage + bootstrapPromise = aliceClient + .getCrypto()! + .bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey }); - // Wait for the key to be uploaded in the account data - secretStorageKey = await awaitSecretStorageKeyStoredInAccountData(); + // Wait for the key to be uploaded in the account data + secretStorageKey = await awaitSecretStorageKeyStoredInAccountData(); - // Return the newly created key in the sync response - sendSyncResponse(secretStorageKey); + // Return the newly created key in the sync response + sendSyncResponse(secretStorageKey); - // Wait for bootstrapSecretStorage to finished - await bootstrapPromise; + // Wait for bootstrapSecretStorage to finished + await bootstrapPromise; - // createSecretStorageKey should have been called twice, one time every bootstrapSecretStorage call - expect(createSecretStorageKey).toHaveBeenCalledTimes(2); - }, - ); + // createSecretStorageKey should have been called twice, one time every bootstrapSecretStorage call + expect(createSecretStorageKey).toHaveBeenCalledTimes(2); + }); - newBackendOnly("should upload cross signing keys", async () => { + it("should upload cross signing keys", async () => { mockSetupCrossSigningRequests(); // Before setting up secret-storage, bootstrap cross-signing, so that the client has cross-signing keys. - await aliceClient.getCrypto()?.bootstrapCrossSigning({}); + await aliceClient.getCrypto()!.bootstrapCrossSigning({}); // Now, when we bootstrap secret-storage, the cross-signing keys should be uploaded. const bootstrapPromise = aliceClient @@ -2457,16 +2453,24 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, expect(selfSigningKey[secretStorageKey]).toBeDefined(); }); - oldBackendOnly("should create a new megolm backup", async () => { + it("should create a new megolm backup", async () => { const backupVersion = "abc"; await bootstrapSecurity(backupVersion); // Expect a backup to be available and used const activeBackup = await aliceClient.getCrypto()!.getActiveSessionBackupVersion(); expect(activeBackup).toStrictEqual(backupVersion); + + // check that there is a MSK signature + const signatures = (await aliceClient.getCrypto()!.checkKeyBackupAndEnable())!.backupInfo.auth_data! + .signatures; + expect(signatures).toBeDefined(); + expect(signatures![aliceClient.getUserId()!]).toBeDefined(); + const mskId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.Master)!; + expect(signatures![aliceClient.getUserId()!][`ed25519:${mskId}`]).toBeDefined(); }); - oldBackendOnly("Reset key backup should create a new backup and update 4S", async () => { + it("Reset key backup should create a new backup and update 4S", async () => { // First set up 4S and key backup const backupVersion = "1"; await bootstrapSecurity(backupVersion); @@ -2539,10 +2543,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, expect(nextVersion).not.toEqual(currentVersion); expect(nextKey).not.toEqual(currentBackupKey); - // Test deletion of the backup - await aliceClient.getCrypto()!.deleteKeyBackupVersion(nextVersion!); + // The `deleteKeyBackupVersion` API is deprecated but has been modified to work with both crypto backend + // ensure that it works anyhow + await aliceClient.deleteKeyBackupVersion(nextVersion!); await aliceClient.getCrypto()!.checkKeyBackupAndEnable(); - // XXX Legacy crypto does not update 4S when deleting backup; should ensure that rust implem does it. + // XXX Legacy crypto does not update 4S when doing that; should ensure that rust implem does it. expect(await aliceClient.getCrypto()!.getActiveSessionBackupVersion()).toBeNull(); }); }); diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index 2f0bcd008..ac4183805 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -22,9 +22,24 @@ import { logger } from "../logger"; import { ClientPrefix, IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api"; import { CryptoEvent } from "../crypto"; import { TypedEventEmitter } from "../models/typed-event-emitter"; +import { encodeUri } from "../utils"; import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor"; import { sleep } from "../utils"; +/** Authentification of the backup info, depends on algorithm */ +type AuthData = KeyBackupInfo["auth_data"]; + +/** + * Holds information of a created keybackup. + * Useful to get the generated private key material and save it securely somewhere. + */ +interface KeyBackupCreationInfo { + version: string; + algorithm: string; + authData: AuthData; + decryptionKey: RustSdkCryptoJs.BackupDecryptionKey; +} + /** * @internal */ @@ -280,6 +295,79 @@ export class RustBackupManager extends TypedEventEmitter Promise): Promise { + // Clean up any existing backup + await this.deleteAllKeyBackupVersions(); + + const randomKey = RustSdkCryptoJs.BackupDecryptionKey.createRandomKey(); + const pubKey = randomKey.megolmV1PublicKey; + + const authData = { public_key: pubKey.publicKeyBase64 }; + + await signObject(authData); + + const res = await this.http.authedRequest<{ version: string }>( + Method.Post, + "/room_keys/version", + undefined, + { + algorithm: pubKey.algorithm, + auth_data: authData, + }, + { + prefix: ClientPrefix.V3, + }, + ); + + this.olmMachine.saveBackupDecryptionKey(randomKey, res.version); + + return { + version: res.version, + algorithm: pubKey.algorithm, + authData: authData, + decryptionKey: randomKey, + }; + } + + /** + * Deletes all key backups. + * + * Will call the API to delete active backup until there is no more present. + */ + public async deleteAllKeyBackupVersions(): Promise { + // there could be several backup versions. Delete all to be safe. + let current = (await this.requestKeyBackupVersion())?.version ?? null; + while (current != null) { + await this.deleteKeyBackupVersion(current); + current = (await this.requestKeyBackupVersion())?.version ?? null; + } + + // XXX: Should this also update Secret Storage and delete any existing keys? + } + + /** + * Deletes the given key backup. + * + * @param version - The backup version to delete. + */ + public async deleteKeyBackupVersion(version: string): Promise { + logger.debug(`deleteKeyBackupVersion v:${version}`); + const path = encodeUri("/room_keys/version/$version", { $version: version }); + await this.http.authedRequest(Method.Delete, path, undefined, undefined, { + prefix: ClientPrefix.V3, + }); + } } export type RustBackupCryptoEvents = diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index a76e349d6..1478271a3 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import anotherjson from "another-json"; import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm"; import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto"; @@ -63,9 +64,15 @@ import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupManager } f import { TypedReEmitter } from "../ReEmitter"; import { randomString } from "../randomstring"; import { ClientStoppedError } from "../errors"; +import { ISignatures } from "../@types/signed"; const ALL_VERIFICATION_METHODS = ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"]; +interface ISignableObject { + signatures?: ISignatures; + unsigned?: object; +} + /** * An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto. * @@ -555,6 +562,7 @@ export class RustCrypto extends TypedEventEmitter { // If an AES Key is already stored in the secret storage and setupNewSecretStorage is not set // we don't want to create a new key @@ -598,6 +606,10 @@ export class RustCrypto extends TypedEventEmitter { - // stub - } - /** * Implementation of {@link CryptoApi#deleteKeyBackupVersion}. */ public async deleteKeyBackupVersion(version: string): Promise { - // stub + await this.backupManager.deleteKeyBackupVersion(version); + } + + /** + * Implementation of {@link CryptoApi#resetKeyBackup}. + */ + public async resetKeyBackup(): Promise { + const backupInfo = await this.backupManager.setupKeyBackup((o) => this.signObject(o)); + + // we want to store the private key in 4S + // need to check if 4S is set up? + if (await this.secretStorageHasAESKey()) { + await this.secretStorage.store("m.megolm_backup.v1", backupInfo.decryptionKey.toBase64()); + } + + // we can check and start async + this.checkKeyBackupAndEnable(); + } + + /** + * Signs the given object with the current device and current identity (if available). + * As defined in {@link https://spec.matrix.org/v1.8/appendices/#signing-json | Signing JSON}. + * + * @param obj - The object to sign + */ + private async signObject(obj: T): Promise { + const sigs = new Map(Object.entries(obj.signatures || {})); + const unsigned = obj.unsigned; + + delete obj.signatures; + delete obj.unsigned; + + const userSignatures = sigs.get(this.userId) || {}; + + const canonalizedJson = anotherjson.stringify(obj); + const signatures: RustSdkCryptoJs.Signatures = await this.olmMachine.sign(canonalizedJson); + + const map = JSON.parse(signatures.asJSON()); + + sigs.set(this.userId, { ...userSignatures, ...map[this.userId] }); + + if (unsigned !== undefined) obj.unsigned = unsigned; + obj.signatures = Object.fromEntries(sigs.entries()); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////