Skip to content

Commit d7831f9

Browse files
authored
Implement key backup APIs for rust and create backup in bootstrapSecretStorage (#3690)
* new resetKeyBackup API * add delete backup version test * code review * support backup creation in rust * code review
1 parent 989c5a3 commit d7831f9

File tree

3 files changed

+181
-41
lines changed

3 files changed

+181
-41
lines changed

spec/integ/crypto/crypto.spec.ts

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ import {
6868
mockSetupMegolmBackupRequests,
6969
} from "../../test-utils/mockEndpoints";
7070
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
71-
import { CryptoCallbacks, KeyBackupInfo } from "../../../src/crypto-api";
71+
import { CrossSigningKey, CryptoCallbacks, KeyBackupInfo } from "../../../src/crypto-api";
7272
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
7373

7474
afterEach(() => {
@@ -2247,7 +2247,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
22472247
}
22482248

22492249
/**
2250-
* Add all mocks needed to set up cross-signing, key backup, 4S and then
2250+
* Add all mocks needed to setup cross-signing, key backup, 4S and then
22512251
* configure the account to have recovery.
22522252
*
22532253
* @param backupVersion - The version of the created backup
@@ -2295,7 +2295,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
22952295
await bootstrapPromise;
22962296
// Finally ensure backup is working
22972297
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
2298-
22992298
await backupStatusUpdate;
23002299
}
23012300

@@ -2346,7 +2345,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
23462345
},
23472346
);
23482347

2349-
newBackendOnly("should create a new key", async () => {
2348+
it("should create a new key", async () => {
23502349
const bootstrapPromise = aliceClient
23512350
.getCrypto()!
23522351
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
@@ -2389,46 +2388,43 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
23892388
},
23902389
);
23912390

2392-
newBackendOnly(
2393-
"should create a new key if setupNewSecretStorage is at true even if an AES key is already in the secret storage",
2394-
async () => {
2395-
let bootstrapPromise = aliceClient
2396-
.getCrypto()!
2397-
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
2391+
it("should create a new key if setupNewSecretStorage is at true even if an AES key is already in the secret storage", async () => {
2392+
let bootstrapPromise = aliceClient
2393+
.getCrypto()!
2394+
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
23982395

2399-
// Wait for the key to be uploaded in the account data
2400-
let secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
2396+
// Wait for the key to be uploaded in the account data
2397+
let secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
24012398

2402-
// Return the newly created key in the sync response
2403-
sendSyncResponse(secretStorageKey);
2399+
// Return the newly created key in the sync response
2400+
sendSyncResponse(secretStorageKey);
24042401

2405-
// Wait for bootstrapSecretStorage to finished
2406-
await bootstrapPromise;
2402+
// Wait for bootstrapSecretStorage to finished
2403+
await bootstrapPromise;
24072404

2408-
// Call again bootstrapSecretStorage
2409-
bootstrapPromise = aliceClient
2410-
.getCrypto()!
2411-
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
2405+
// Call again bootstrapSecretStorage
2406+
bootstrapPromise = aliceClient
2407+
.getCrypto()!
2408+
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
24122409

2413-
// Wait for the key to be uploaded in the account data
2414-
secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
2410+
// Wait for the key to be uploaded in the account data
2411+
secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
24152412

2416-
// Return the newly created key in the sync response
2417-
sendSyncResponse(secretStorageKey);
2413+
// Return the newly created key in the sync response
2414+
sendSyncResponse(secretStorageKey);
24182415

2419-
// Wait for bootstrapSecretStorage to finished
2420-
await bootstrapPromise;
2416+
// Wait for bootstrapSecretStorage to finished
2417+
await bootstrapPromise;
24212418

2422-
// createSecretStorageKey should have been called twice, one time every bootstrapSecretStorage call
2423-
expect(createSecretStorageKey).toHaveBeenCalledTimes(2);
2424-
},
2425-
);
2419+
// createSecretStorageKey should have been called twice, one time every bootstrapSecretStorage call
2420+
expect(createSecretStorageKey).toHaveBeenCalledTimes(2);
2421+
});
24262422

2427-
newBackendOnly("should upload cross signing keys", async () => {
2423+
it("should upload cross signing keys", async () => {
24282424
mockSetupCrossSigningRequests();
24292425

24302426
// Before setting up secret-storage, bootstrap cross-signing, so that the client has cross-signing keys.
2431-
await aliceClient.getCrypto()?.bootstrapCrossSigning({});
2427+
await aliceClient.getCrypto()!.bootstrapCrossSigning({});
24322428

24332429
// Now, when we bootstrap secret-storage, the cross-signing keys should be uploaded.
24342430
const bootstrapPromise = aliceClient
@@ -2457,16 +2453,24 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
24572453
expect(selfSigningKey[secretStorageKey]).toBeDefined();
24582454
});
24592455

2460-
oldBackendOnly("should create a new megolm backup", async () => {
2456+
it("should create a new megolm backup", async () => {
24612457
const backupVersion = "abc";
24622458
await bootstrapSecurity(backupVersion);
24632459

24642460
// Expect a backup to be available and used
24652461
const activeBackup = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
24662462
expect(activeBackup).toStrictEqual(backupVersion);
2463+
2464+
// check that there is a MSK signature
2465+
const signatures = (await aliceClient.getCrypto()!.checkKeyBackupAndEnable())!.backupInfo.auth_data!
2466+
.signatures;
2467+
expect(signatures).toBeDefined();
2468+
expect(signatures![aliceClient.getUserId()!]).toBeDefined();
2469+
const mskId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.Master)!;
2470+
expect(signatures![aliceClient.getUserId()!][`ed25519:${mskId}`]).toBeDefined();
24672471
});
24682472

2469-
oldBackendOnly("Reset key backup should create a new backup and update 4S", async () => {
2473+
it("Reset key backup should create a new backup and update 4S", async () => {
24702474
// First set up 4S and key backup
24712475
const backupVersion = "1";
24722476
await bootstrapSecurity(backupVersion);
@@ -2539,10 +2543,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
25392543
expect(nextVersion).not.toEqual(currentVersion);
25402544
expect(nextKey).not.toEqual(currentBackupKey);
25412545

2542-
// Test deletion of the backup
2543-
await aliceClient.getCrypto()!.deleteKeyBackupVersion(nextVersion!);
2546+
// The `deleteKeyBackupVersion` API is deprecated but has been modified to work with both crypto backend
2547+
// ensure that it works anyhow
2548+
await aliceClient.deleteKeyBackupVersion(nextVersion!);
25442549
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
2545-
// XXX Legacy crypto does not update 4S when deleting backup; should ensure that rust implem does it.
2550+
// XXX Legacy crypto does not update 4S when doing that; should ensure that rust implem does it.
25462551
expect(await aliceClient.getCrypto()!.getActiveSessionBackupVersion()).toBeNull();
25472552
});
25482553
});

src/rust-crypto/backup.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,24 @@ import { logger } from "../logger";
2222
import { ClientPrefix, IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api";
2323
import { CryptoEvent } from "../crypto";
2424
import { TypedEventEmitter } from "../models/typed-event-emitter";
25+
import { encodeUri } from "../utils";
2526
import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
2627
import { sleep } from "../utils";
2728

29+
/** Authentification of the backup info, depends on algorithm */
30+
type AuthData = KeyBackupInfo["auth_data"];
31+
32+
/**
33+
* Holds information of a created keybackup.
34+
* Useful to get the generated private key material and save it securely somewhere.
35+
*/
36+
interface KeyBackupCreationInfo {
37+
version: string;
38+
algorithm: string;
39+
authData: AuthData;
40+
decryptionKey: RustSdkCryptoJs.BackupDecryptionKey;
41+
}
42+
2843
/**
2944
* @internal
3045
*/
@@ -280,6 +295,79 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
280295
}
281296
}
282297
}
298+
299+
/**
300+
* Creates a new key backup by generating a new random private key.
301+
*
302+
* If there is an existing backup server side it will be deleted and replaced
303+
* by the new one.
304+
*
305+
* @param signObject - Method that should sign the backup with existing device and
306+
* existing identity.
307+
* @returns a KeyBackupCreationInfo - All information related to the backup.
308+
*/
309+
public async setupKeyBackup(signObject: (authData: AuthData) => Promise<void>): Promise<KeyBackupCreationInfo> {
310+
// Clean up any existing backup
311+
await this.deleteAllKeyBackupVersions();
312+
313+
const randomKey = RustSdkCryptoJs.BackupDecryptionKey.createRandomKey();
314+
const pubKey = randomKey.megolmV1PublicKey;
315+
316+
const authData = { public_key: pubKey.publicKeyBase64 };
317+
318+
await signObject(authData);
319+
320+
const res = await this.http.authedRequest<{ version: string }>(
321+
Method.Post,
322+
"/room_keys/version",
323+
undefined,
324+
{
325+
algorithm: pubKey.algorithm,
326+
auth_data: authData,
327+
},
328+
{
329+
prefix: ClientPrefix.V3,
330+
},
331+
);
332+
333+
this.olmMachine.saveBackupDecryptionKey(randomKey, res.version);
334+
335+
return {
336+
version: res.version,
337+
algorithm: pubKey.algorithm,
338+
authData: authData,
339+
decryptionKey: randomKey,
340+
};
341+
}
342+
343+
/**
344+
* Deletes all key backups.
345+
*
346+
* Will call the API to delete active backup until there is no more present.
347+
*/
348+
public async deleteAllKeyBackupVersions(): Promise<void> {
349+
// there could be several backup versions. Delete all to be safe.
350+
let current = (await this.requestKeyBackupVersion())?.version ?? null;
351+
while (current != null) {
352+
await this.deleteKeyBackupVersion(current);
353+
current = (await this.requestKeyBackupVersion())?.version ?? null;
354+
}
355+
356+
// XXX: Should this also update Secret Storage and delete any existing keys?
357+
}
358+
359+
/**
360+
* Deletes the given key backup.
361+
*
362+
* @param version - The backup version to delete.
363+
*/
364+
public async deleteKeyBackupVersion(version: string): Promise<void> {
365+
logger.debug(`deleteKeyBackupVersion v:${version}`);
366+
const path = encodeUri("/room_keys/version/$version", { $version: version });
367+
await this.http.authedRequest<void>(Method.Delete, path, undefined, undefined, {
368+
prefix: ClientPrefix.V3,
369+
});
370+
}
283371
}
284372

285373
export type RustBackupCryptoEvents =

src/rust-crypto/rust-crypto.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import anotherjson from "another-json";
1718
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
1819

1920
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
@@ -63,9 +64,15 @@ import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupManager } f
6364
import { TypedReEmitter } from "../ReEmitter";
6465
import { randomString } from "../randomstring";
6566
import { ClientStoppedError } from "../errors";
67+
import { ISignatures } from "../@types/signed";
6668

6769
const ALL_VERIFICATION_METHODS = ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"];
6870

71+
interface ISignableObject {
72+
signatures?: ISignatures;
73+
unsigned?: object;
74+
}
75+
6976
/**
7077
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
7178
*
@@ -555,6 +562,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
555562
public async bootstrapSecretStorage({
556563
createSecretStorageKey,
557564
setupNewSecretStorage,
565+
setupNewKeyBackup,
558566
}: CreateSecretStorageOpts = {}): Promise<void> {
559567
// If an AES Key is already stored in the secret storage and setupNewSecretStorage is not set
560568
// we don't want to create a new key
@@ -598,6 +606,10 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
598606
await this.secretStorage.store("m.cross_signing.master", crossSigningPrivateKeys.masterKey);
599607
await this.secretStorage.store("m.cross_signing.user_signing", crossSigningPrivateKeys.userSigningKey);
600608
await this.secretStorage.store("m.cross_signing.self_signing", crossSigningPrivateKeys.self_signing_key);
609+
610+
if (setupNewKeyBackup) {
611+
await this.resetKeyBackup();
612+
}
601613
}
602614
}
603615

@@ -938,18 +950,53 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
938950
return await this.backupManager.checkKeyBackupAndEnable(true);
939951
}
940952

953+
/**
954+
* Implementation of {@link CryptoApi#deleteKeyBackupVersion}.
955+
*/
956+
public async deleteKeyBackupVersion(version: string): Promise<void> {
957+
await this.backupManager.deleteKeyBackupVersion(version);
958+
}
959+
941960
/**
942961
* Implementation of {@link CryptoApi#resetKeyBackup}.
943962
*/
944963
public async resetKeyBackup(): Promise<void> {
945-
// stub
964+
const backupInfo = await this.backupManager.setupKeyBackup((o) => this.signObject(o));
965+
966+
// we want to store the private key in 4S
967+
// need to check if 4S is set up?
968+
if (await this.secretStorageHasAESKey()) {
969+
await this.secretStorage.store("m.megolm_backup.v1", backupInfo.decryptionKey.toBase64());
970+
}
971+
972+
// we can check and start async
973+
this.checkKeyBackupAndEnable();
946974
}
947975

948976
/**
949-
* Implementation of {@link CryptoApi#deleteKeyBackupVersion}.
977+
* Signs the given object with the current device and current identity (if available).
978+
* As defined in {@link https://spec.matrix.org/v1.8/appendices/#signing-json | Signing JSON}.
979+
*
980+
* @param obj - The object to sign
950981
*/
951-
public async deleteKeyBackupVersion(version: string): Promise<void> {
952-
// stub
982+
private async signObject<T extends ISignableObject & object>(obj: T): Promise<void> {
983+
const sigs = new Map(Object.entries(obj.signatures || {}));
984+
const unsigned = obj.unsigned;
985+
986+
delete obj.signatures;
987+
delete obj.unsigned;
988+
989+
const userSignatures = sigs.get(this.userId) || {};
990+
991+
const canonalizedJson = anotherjson.stringify(obj);
992+
const signatures: RustSdkCryptoJs.Signatures = await this.olmMachine.sign(canonalizedJson);
993+
994+
const map = JSON.parse(signatures.asJSON());
995+
996+
sigs.set(this.userId, { ...userSignatures, ...map[this.userId] });
997+
998+
if (unsigned !== undefined) obj.unsigned = unsigned;
999+
obj.signatures = Object.fromEntries(sigs.entries());
9531000
}
9541001

9551002
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

0 commit comments

Comments
 (0)