diff --git a/spec/integ/crypto/crypto.spec.ts b/spec/integ/crypto/crypto.spec.ts index c6009c18ead..003e8f6faca 100644 --- a/spec/integ/crypto/crypto.spec.ts +++ b/spec/integ/crypto/crypto.spec.ts @@ -54,6 +54,7 @@ import { Room, RoomMember, RoomStateEvent, + CryptoEvent, } from "../../../src/matrix"; import { DeviceInfo } from "../../../src/crypto/deviceinfo"; import { E2EKeyReceiver, IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver"; @@ -61,9 +62,13 @@ import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder"; import { escapeRegExp } from "../../../src/utils"; import { downloadDeviceToJsDevice } from "../../../src/rust-crypto/device-converter"; import { flushPromises } from "../../test-utils/flushPromises"; -import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints"; +import { + mockInitialApiRequests, + mockSetupCrossSigningRequests, + mockSetupMegolmBackupRequests, +} from "../../test-utils/mockEndpoints"; import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage"; -import { CryptoCallbacks } from "../../../src/crypto-api"; +import { CryptoCallbacks, KeyBackupInfo } from "../../../src/crypto-api"; import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder"; afterEach(() => { @@ -2228,6 +2233,65 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, }); } + function awaitMegolmBackupKeyUpload(): Promise> { + return new Promise((resolve) => { + // Called when the megolm backup key is uploaded + fetchMock.put( + `express:/_matrix/client/v3/user/:userId/account_data/m.megolm_backup.v1`, + (url: string, options: RequestInit) => { + const content = JSON.parse(options.body as string); + resolve(content.encrypted); + return {}; + }, + { overwriteRoutes: true }, + ); + }); + } + + async function bootstrapSecurity(backupVersion: string): Promise { + mockSetupCrossSigningRequests(); + mockSetupMegolmBackupRequests(backupVersion); + + // promise which will resolve when a `KeyBackupStatus` event is emitted with `enabled: true` + const backupStatusUpdate = new Promise((resolve) => { + aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => { + if (enabled) { + resolve(); + } + }); + }); + + const setupPromises = [ + awaitCrossSigningKeyUpload("master"), + awaitCrossSigningKeyUpload("user_signing"), + awaitCrossSigningKeyUpload("self_signing"), + awaitMegolmBackupKeyUpload(), + ]; + + // Before setting up secret-storage, bootstrap cross-signing, so that the client has cross-signing keys. + await aliceClient.getCrypto()!.bootstrapCrossSigning({}); + + // Now, when we bootstrap secret-storage, the cross-signing keys should be uploaded. + const bootstrapPromise = aliceClient.getCrypto()!.bootstrapSecretStorage({ + setupNewSecretStorage: true, + createSecretStorageKey, + setupNewKeyBackup: true, + }); + + // Wait for the key to be uploaded in the account data + const secretStorageKey = await awaitSecretStorageKeyStoredInAccountData(); + + // Return the newly created key in the sync response + sendSyncResponse(secretStorageKey); + + // Wait for the cross signing keys to be uploaded + await Promise.all(setupPromises); + + // Finally, wait for bootstrapSecretStorage to finished + await bootstrapPromise; + await backupStatusUpdate; + } + /** * Send in the sync response the provided `secretStorageKey` into the account_data field * The key is set for the `m.secret_storage.default_key` and `m.secret_storage.key.${secretStorageKey}` events @@ -2275,7 +2339,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 }); @@ -2318,46 +2382,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 @@ -2385,6 +2446,89 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, expect(userSigningKey[secretStorageKey]).toBeDefined(); expect(selfSigningKey[secretStorageKey]).toBeDefined(); }); + + newBackendOnly("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); + }); + + it("Reset key backup should create a new backup and update 4S", async () => { + // First set up recovery + const backupVersion = "1"; + await bootstrapSecurity(backupVersion); + + const currentVersion = await aliceClient.getCrypto()!.getActiveSessionBackupVersion(); + const currentBackupKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey(); + + // we will call reset backup, it should delete the existing one, then setup a new one + // Let's mock for that + + // Mock delete and replace the GET to return 404 as soon as called + const awaitDeleteCalled = new Promise((resolve) => { + fetchMock.delete( + "express:/_matrix/client/v3/room_keys/version/:version", + (url: string, options: RequestInit) => { + fetchMock.get( + "path:/_matrix/client/v3/room_keys/version", + { + status: 404, + body: { errcode: "M_NOT_FOUND", error: "Account data not found." }, + }, + { overwriteRoutes: true }, + ); + resolve(); + return {}; + }, + { overwriteRoutes: true }, + ); + }); + + const newVersion = "2"; + fetchMock.post( + "path:/_matrix/client/v3/room_keys/version", + (url, request) => { + const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}"); + backupData.version = newVersion; + backupData.count = 0; + backupData.etag = "zer"; + + // update get call with new version + fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, { + overwriteRoutes: true, + }); + return { + version: backupVersion, + }; + }, + { overwriteRoutes: true }, + ); + + const newBackupStatusUpdate = new Promise((resolve) => { + aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => { + if (enabled) { + resolve(); + } + }); + }); + + const new4SUpload = awaitMegolmBackupKeyUpload(); + + await aliceClient.getCrypto()!.resetKeyBackup(); + await awaitDeleteCalled; + await newBackupStatusUpdate; + await new4SUpload; + + const nextVersion = await aliceClient.getCrypto()!.getActiveSessionBackupVersion(); + const nextKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey(); + + expect(nextVersion).toBeDefined(); + expect(nextVersion).not.toEqual(currentVersion); + expect(nextKey).not.toEqual(currentBackupKey); + }); }); describe("Incoming verification in a DM", () => { diff --git a/spec/test-utils/mockEndpoints.ts b/spec/test-utils/mockEndpoints.ts index 4fdcc5b1a81..c75e758cdda 100644 --- a/spec/test-utils/mockEndpoints.ts +++ b/spec/test-utils/mockEndpoints.ts @@ -16,6 +16,8 @@ limitations under the License. import fetchMock from "fetch-mock-jest"; +import { KeyBackupInfo } from "../../src/crypto-api"; + /** * Mock out the endpoints that the js-sdk calls when we call `MatrixClient.start()`. * @@ -56,3 +58,35 @@ export function mockSetupCrossSigningRequests(): void { {}, ); } + +/** + * Mock out requests to `/room_keys/version`. + * + * Returns `404 M_NOT_FOUND` for GET requests until `POST room_keys/version` is called. + * Once the POST is done, `GET /room_keys/version` will return the posted backup + * instead of 404. + * + * @param backupVersion - The version of the backup to create + */ +export function mockSetupMegolmBackupRequests(backupVersion: string): void { + fetchMock.get("path:/_matrix/client/v3/room_keys/version", { + status: 404, + body: { + errcode: "M_NOT_FOUND", + error: "No current backup version", + }, + }); + + fetchMock.post("path:/_matrix/client/v3/room_keys/version", (url, request) => { + const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}"); + backupData.version = backupVersion; + backupData.count = 0; + backupData.etag = "zer"; + fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, { + overwriteRoutes: true, + }); + return { + version: backupVersion, + }; + }); +} diff --git a/src/client.ts b/src/client.ts index 696ff8140cc..648f33d141b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3270,6 +3270,7 @@ export class MatrixClient extends TypedEventEmitter { let res: IKeyBackupInfo; @@ -3341,6 +3342,7 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -3448,24 +3454,15 @@ export class MatrixClient extends TypedEventEmitter { - if (!this.crypto) { + if (!this.cryptoBackend) { throw new Error("End-to-end encryption disabled"); } - // If we're currently backing up to this backup... stop. - // (We start using it automatically in createKeyBackupVersion - // so this is symmetrical). - // TODO: convert this to use crypto.getActiveSessionBackupVersion. And actually check the version. - if (this.crypto.backupManager.version) { - this.crypto.backupManager.disableKeyBackup(); - } - - const path = utils.encodeUri("/room_keys/version/$version", { - $version: version, - }); - - await this.http.authedRequest(Method.Delete, path, undefined, undefined, { prefix: ClientPrefix.V3 }); + await this.cryptoBackend!.deleteKeyBackupVersion(version); } private makeKeyBackupPath(roomId: undefined, sessionId: undefined, version?: string): IKeyBackupPath; diff --git a/src/crypto-api.ts b/src/crypto-api.ts index 10638d21bc7..fdca02632b8 100644 --- a/src/crypto-api.ts +++ b/src/crypto-api.ts @@ -233,6 +233,24 @@ export interface CryptoApi { */ bootstrapSecretStorage(opts: CreateSecretStorageOpts): Promise; + /** + * Creates a new key backup version. + * + * If there are existing backups they will be replaced. + * + * The decryption key will be saved in Secret Storage (the `SecretStorageCallbacks.getSecretStorageKey` Crypto + * callback will be called) + * and the backup engine will be started. + */ + resetKeyBackup(): Promise; + + /** + * Deletes the given key backup. + * + * @param version - The backup version to delete. + */ + deleteKeyBackupVersion(version: string): Promise; + /** * Get the status of our cross-signing keys. * diff --git a/src/crypto/EncryptionSetup.ts b/src/crypto/EncryptionSetup.ts index 2c838446753..c3e2b652770 100644 --- a/src/crypto/EncryptionSetup.ts +++ b/src/crypto/EncryptionSetup.ts @@ -228,6 +228,8 @@ export class EncryptionSetupOperation { prefix: ClientPrefix.V3, }); } + // enable it + await crypto.backupManager.checkAndStart(); } } } diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index e3f3cf4b1e2..de89bad5d68 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -25,7 +25,7 @@ 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 { encodeUri, safeSet, sleep } from "../utils"; import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store"; import { encodeRecoveryKey } from "./recoverykey"; import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes"; @@ -39,7 +39,7 @@ import { import { UnstableValue } from "../NamespacedValue"; import { CryptoEvent } from "./index"; import { crypto } from "./crypto"; -import { HTTPError, MatrixError } from "../http-api"; +import { ClientPrefix, HTTPError, MatrixError, Method } from "../http-api"; import { BackupTrustInfo } from "../crypto-api/keybackup"; const KEY_BACKUP_KEYS_PER_REQUEST = 200; @@ -224,6 +224,33 @@ export class BackupManager { this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey); } + /** + * 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.baseApis.getKeyBackupVersion())?.version ?? null; + while (current != null) { + await this.deleteKeyBackupVersion(current); + this.disableKeyBackup(); + current = (await this.baseApis.getKeyBackupVersion())?.version ?? null; + } + } + + /** + * Deletes the given key backup. + * + * @param version - The backup version to delete. + */ + public async deleteKeyBackupVersion(version: string): Promise { + const path = encodeUri("/room_keys/version/$version", { $version: version }); + await this.baseApis.http.authedRequest(Method.Delete, path, undefined, undefined, { + prefix: ClientPrefix.V3, + }); + } + /** * Check the server for an active key backup and * if one is present and has a valid signature from diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 8e6c339f6dd..82c2dd3251e 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -98,6 +98,7 @@ import { } from "../crypto-api"; import { Device, DeviceMap } from "../models/device"; import { deviceInfoToDevice } from "./device-converter"; +import { ClientPrefix, Method } from "../http-api"; /* re-exports for backwards compatibility */ export type { @@ -542,6 +543,15 @@ export class Crypto extends TypedEventEmitter { + await this.backupManager.deleteKeyBackupVersion(version); + } + /** * Initialise the crypto module so that it is ready for use * @@ -1088,7 +1098,7 @@ export class Crypto extends TypedEventEmitter { + // Delete existing ones + // There is no use case for having several key backup version live server side. + // Even if not deleted it would be lost as the key to restore is lost. + // There should be only one backup at a time. + await this.backupManager.deleteAllKeyBackupVersions(); + + const info = await this.backupManager.prepareKeyBackupVersion(); + + await this.signObject(info.auth_data); + + // add new key backup + const { version } = await this.baseApis.http.authedRequest<{ version: string }>( + Method.Post, + "/room_keys/version", + undefined, + info, + { + prefix: ClientPrefix.V3, + }, + ); + + logger.log(`Created backup version ${version}`); + + // write the key to 4S + const privateKey = info.privateKey; + await this.secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey)); + await this.storeSessionBackupPrivateKey(privateKey); + + await this.backupManager.checkAndStart(); + } + /** * @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#addKey}. */ diff --git a/src/rust-crypto/backup.ts b/src/rust-crypto/backup.ts index 2f0bcd0082d..4f501ca821e 100644 --- a/src/rust-crypto/backup.ts +++ b/src/rust-crypto/backup.ts @@ -22,9 +22,37 @@ 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"; +/** + * prepareKeyBackupVersion result. + */ +interface PreparedKeyBackupVersion { + /** The prepared algorithm version */ + algorithm: string; + /** The auth data of the algorithm */ + /* eslint-disable-next-line camelcase */ + auth_data: AuthData; + /** The generated private key */ + decryptionKey: RustSdkCryptoJs.BackupDecryptionKey; +} + +/** 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 +308,90 @@ export class RustBackupManager extends TypedEventEmitter Promise): Promise { + // Cleanup any existing backup + await this.deleteAllKeyBackupVersions(); + + const version = await this.prepareKeyBackupVersion(); + await signObject(version.auth_data); + + const res = await this.http.authedRequest<{ version: string }>( + Method.Post, + "/room_keys/version", + undefined, + { + algorithm: version.algorithm, + auth_data: version.auth_data, + }, + { + prefix: ClientPrefix.V3, + }, + ); + + this.olmMachine.saveBackupDecryptionKey(version.decryptionKey, res.version); + + return { + version: res.version, + algorithm: version.algorithm, + authData: version.auth_data, + decryptionKey: version.decryptionKey, + }; + } + + /** + * 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, + }); + } + + /** + * Prepare the keybackup version data, auth_data not signed at this point + * @returns a {@link PreparedKeyBackupVersion} with all information about the creation. + */ + private async prepareKeyBackupVersion(): Promise { + const randomKey = RustSdkCryptoJs.BackupDecryptionKey.createRandomKey(); + const pubKey = randomKey.megolmV1PublicKey; + + return { + algorithm: pubKey.algorithm, + auth_data: { public_key: pubKey.publicKeyBase64 }, + decryptionKey: randomKey, + }; + } } export type RustBackupCryptoEvents = diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 3db3878ec55..fc4c9a1fddc 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"; @@ -29,7 +30,7 @@ import { UserTrustLevel } from "../crypto/CrossSigning"; import { RoomEncryptor } from "./RoomEncryptor"; import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor"; import { KeyClaimManager } from "./KeyClaimManager"; -import { MapWithDefault } from "../utils"; +import { MapWithDefault, recursiveMapToObject } from "../utils"; import { BackupTrustInfo, BootstrapCrossSigningOpts, @@ -61,11 +62,17 @@ import { CryptoEvent } from "../crypto"; import { TypedEventEmitter } from "../models/typed-event-emitter"; import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupManager } from "./backup"; import { TypedReEmitter } from "../ReEmitter"; +import { ISignatures } from "../@types/signed"; import { randomString } from "../randomstring"; import { ClientStoppedError } from "../errors"; 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,7 +606,56 @@ export class RustCrypto extends TypedEventEmitter { + 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(); + } + + 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 = recursiveMapToObject(sigs); } /**