Skip to content

Commit dcdbbee

Browse files
committed
support backup creation in rust
1 parent b39741a commit dcdbbee

File tree

3 files changed

+205
-37
lines changed

3 files changed

+205
-37
lines changed

spec/integ/crypto/crypto.spec.ts

+40-34
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ import { logger } from "../../../src/logger";
4040
import {
4141
Category,
4242
createClient,
43-
CryptoEvent,
4443
IClaimOTKsResult,
4544
IContent,
4645
IDownloadKeyResult,
@@ -55,6 +54,7 @@ import {
5554
Room,
5655
RoomMember,
5756
RoomStateEvent,
57+
CryptoEvent,
5858
} from "../../../src/matrix";
5959
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
6060
import { E2EKeyReceiver, IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
@@ -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(() => {
@@ -2202,9 +2202,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
22022202
"express:/_matrix/client/v3/user/:userId/account_data/:type(m.secret_storage.*)",
22032203
(url: string, options: RequestInit) => {
22042204
const content = JSON.parse(options.body as string);
2205+
22052206
if (content.key) {
22062207
resolve(content.key);
22072208
}
2209+
22082210
return {};
22092211
},
22102212
{ overwriteRoutes: true },
@@ -2289,7 +2291,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
22892291
await bootstrapPromise;
22902292
// Finally ensure backup is working
22912293
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
2292-
22932294
await backupStatusUpdate;
22942295
}
22952296

@@ -2340,7 +2341,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
23402341
},
23412342
);
23422343

2343-
newBackendOnly("should create a new key", async () => {
2344+
it("should create a new key", async () => {
23442345
const bootstrapPromise = aliceClient
23452346
.getCrypto()!
23462347
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
@@ -2383,46 +2384,43 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
23832384
},
23842385
);
23852386

2386-
newBackendOnly(
2387-
"should create a new key if setupNewSecretStorage is at true even if an AES key is already in the secret storage",
2388-
async () => {
2389-
let bootstrapPromise = aliceClient
2390-
.getCrypto()!
2391-
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
2387+
it("should create a new key if setupNewSecretStorage is at true even if an AES key is already in the secret storage", async () => {
2388+
let bootstrapPromise = aliceClient
2389+
.getCrypto()!
2390+
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
23922391

2393-
// Wait for the key to be uploaded in the account data
2394-
let secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
2392+
// Wait for the key to be uploaded in the account data
2393+
let secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
23952394

2396-
// Return the newly created key in the sync response
2397-
sendSyncResponse(secretStorageKey);
2395+
// Return the newly created key in the sync response
2396+
sendSyncResponse(secretStorageKey);
23982397

2399-
// Wait for bootstrapSecretStorage to finished
2400-
await bootstrapPromise;
2398+
// Wait for bootstrapSecretStorage to finished
2399+
await bootstrapPromise;
24012400

2402-
// Call again bootstrapSecretStorage
2403-
bootstrapPromise = aliceClient
2404-
.getCrypto()!
2405-
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
2401+
// Call again bootstrapSecretStorage
2402+
bootstrapPromise = aliceClient
2403+
.getCrypto()!
2404+
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
24062405

2407-
// Wait for the key to be uploaded in the account data
2408-
secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
2406+
// Wait for the key to be uploaded in the account data
2407+
secretStorageKey = await awaitSecretStorageKeyStoredInAccountData();
24092408

2410-
// Return the newly created key in the sync response
2411-
sendSyncResponse(secretStorageKey);
2409+
// Return the newly created key in the sync response
2410+
sendSyncResponse(secretStorageKey);
24122411

2413-
// Wait for bootstrapSecretStorage to finished
2414-
await bootstrapPromise;
2412+
// Wait for bootstrapSecretStorage to finished
2413+
await bootstrapPromise;
24152414

2416-
// createSecretStorageKey should have been called twice, one time every bootstrapSecretStorage call
2417-
expect(createSecretStorageKey).toHaveBeenCalledTimes(2);
2418-
},
2419-
);
2415+
// createSecretStorageKey should have been called twice, one time every bootstrapSecretStorage call
2416+
expect(createSecretStorageKey).toHaveBeenCalledTimes(2);
2417+
});
24202418

2421-
newBackendOnly("should upload cross signing keys", async () => {
2419+
it("should upload cross signing keys", async () => {
24222420
mockSetupCrossSigningRequests();
24232421

24242422
// Before setting up secret-storage, bootstrap cross-signing, so that the client has cross-signing keys.
2425-
await aliceClient.getCrypto()?.bootstrapCrossSigning({});
2423+
await aliceClient.getCrypto()!.bootstrapCrossSigning({});
24262424

24272425
// Now, when we bootstrap secret-storage, the cross-signing keys should be uploaded.
24282426
const bootstrapPromise = aliceClient
@@ -2451,16 +2449,24 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
24512449
expect(selfSigningKey[secretStorageKey]).toBeDefined();
24522450
});
24532451

2454-
oldBackendOnly("should create a new megolm backup", async () => {
2452+
it("should create a new megolm backup", async () => {
24552453
const backupVersion = "abc";
24562454
await bootstrapSecurity(backupVersion);
24572455

24582456
// Expect a backup to be available and used
24592457
const activeBackup = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
24602458
expect(activeBackup).toStrictEqual(backupVersion);
2459+
2460+
// check that there is a MSK signature
2461+
const signatures = (await aliceClient.getCrypto()!.checkKeyBackupAndEnable())!.backupInfo.auth_data!
2462+
.signatures;
2463+
expect(signatures).toBeDefined();
2464+
expect(signatures![aliceClient.getUserId()!]).toBeDefined();
2465+
const mskId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.Master)!;
2466+
expect(signatures![aliceClient.getUserId()!][`ed25519:${mskId}`]).toBeDefined();
24612467
});
24622468

2463-
oldBackendOnly("Reset key backup should create a new backup and update 4S", async () => {
2469+
it("Reset key backup should create a new backup and update 4S", async () => {
24642470
// First set up recovery
24652471
const backupVersion = "1";
24662472
await bootstrapSecurity(backupVersion);

src/rust-crypto/backup.ts

+112
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,37 @@ 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+
/**
30+
* prepareKeyBackupVersion result.
31+
*/
32+
interface PreparedKeyBackupVersion {
33+
/** The prepared algorithm version */
34+
algorithm: string;
35+
/** The auth data of the algorithm */
36+
/* eslint-disable-next-line camelcase */
37+
auth_data: AuthData;
38+
/** The generated private key */
39+
decryptionKey: RustSdkCryptoJs.BackupDecryptionKey;
40+
}
41+
42+
/** Authentification of the backup info, depends on algorithm */
43+
type AuthData = KeyBackupInfo["auth_data"];
44+
45+
/**
46+
* Holds information of a created keybackup.
47+
* Useful to get the generated private key material and save it securely somewhere.
48+
*/
49+
interface KeyBackupCreationInfo {
50+
version: string;
51+
algorithm: string;
52+
authData: AuthData;
53+
decryptionKey: RustSdkCryptoJs.BackupDecryptionKey;
54+
}
55+
2856
/**
2957
* @internal
3058
*/
@@ -280,6 +308,90 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
280308
}
281309
}
282310
}
311+
312+
/**
313+
* Creates a new key backup by generating a new random private key.
314+
*
315+
* If there is an existing backup server side it will be deleted and replaced
316+
* by the new one.
317+
*
318+
* @param signObject - Method that should sign the backup with existing device and
319+
* existing identity.
320+
* @returns a KeyBackupCreationInfo - All information related to the backup.
321+
*/
322+
public async setupKeyBackup(signObject: (authData: AuthData) => Promise<void>): Promise<KeyBackupCreationInfo> {
323+
// Cleanup any existing backup
324+
await this.deleteAllKeyBackupVersions();
325+
326+
const version = await this.prepareKeyBackupVersion();
327+
await signObject(version.auth_data);
328+
329+
const res = await this.http.authedRequest<{ version: string }>(
330+
Method.Post,
331+
"/room_keys/version",
332+
undefined,
333+
{
334+
algorithm: version.algorithm,
335+
auth_data: version.auth_data,
336+
},
337+
{
338+
prefix: ClientPrefix.V3,
339+
},
340+
);
341+
342+
this.olmMachine.saveBackupDecryptionKey(version.decryptionKey, res.version);
343+
344+
return {
345+
version: res.version,
346+
algorithm: version.algorithm,
347+
authData: version.auth_data,
348+
decryptionKey: version.decryptionKey,
349+
};
350+
}
351+
352+
/**
353+
* Deletes all key backups.
354+
*
355+
* Will call the API to delete active backup until there is no more present.
356+
*/
357+
public async deleteAllKeyBackupVersions(): Promise<void> {
358+
// there could be several backup versions. Delete all to be safe.
359+
let current = (await this.requestKeyBackupVersion())?.version ?? null;
360+
while (current != null) {
361+
await this.deleteKeyBackupVersion(current);
362+
current = (await this.requestKeyBackupVersion())?.version ?? null;
363+
}
364+
365+
// XXX: Should this also update Secret Storage and delete any existing keys?
366+
}
367+
368+
/**
369+
* Deletes the given key backup.
370+
*
371+
* @param version - The backup version to delete.
372+
*/
373+
public async deleteKeyBackupVersion(version: string): Promise<void> {
374+
logger.debug(`deleteKeyBackupVersion v:${version}`);
375+
const path = encodeUri("/room_keys/version/$version", { $version: version });
376+
await this.http.authedRequest<void>(Method.Delete, path, undefined, undefined, {
377+
prefix: ClientPrefix.V3,
378+
});
379+
}
380+
381+
/**
382+
* Prepare the keybackup version data, auth_data not signed at this point
383+
* @returns a {@link PreparedKeyBackupVersion} with all information about the creation.
384+
*/
385+
private async prepareKeyBackupVersion(): Promise<PreparedKeyBackupVersion> {
386+
const randomKey = RustSdkCryptoJs.BackupDecryptionKey.createRandomKey();
387+
const pubKey = randomKey.megolmV1PublicKey;
388+
389+
return {
390+
algorithm: pubKey.algorithm,
391+
auth_data: { public_key: pubKey.publicKeyBase64 },
392+
decryptionKey: randomKey,
393+
};
394+
}
283395
}
284396

285397
export type RustBackupCryptoEvents =

src/rust-crypto/rust-crypto.ts

+53-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
16+
/**
17+
* Utilities common to olm encryption algorithms
18+
*/
1619

20+
import anotherjson from "another-json";
1721
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
1822

1923
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
@@ -63,9 +67,15 @@ import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupManager } f
6367
import { TypedReEmitter } from "../ReEmitter";
6468
import { randomString } from "../randomstring";
6569
import { ClientStoppedError } from "../errors";
70+
import { ISignatures } from "../@types/signed";
6671

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

74+
interface ISignableObject {
75+
signatures?: ISignatures;
76+
unsigned?: object;
77+
}
78+
6979
/**
7080
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
7181
*
@@ -555,6 +565,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
555565
public async bootstrapSecretStorage({
556566
createSecretStorageKey,
557567
setupNewSecretStorage,
568+
setupNewKeyBackup,
558569
}: CreateSecretStorageOpts = {}): Promise<void> {
559570
// If an AES Key is already stored in the secret storage and setupNewSecretStorage is not set
560571
// we don't want to create a new key
@@ -598,19 +609,58 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
598609
await this.secretStorage.store("m.cross_signing.master", crossSigningPrivateKeys.masterKey);
599610
await this.secretStorage.store("m.cross_signing.user_signing", crossSigningPrivateKeys.userSigningKey);
600611
await this.secretStorage.store("m.cross_signing.self_signing", crossSigningPrivateKeys.self_signing_key);
612+
613+
if (setupNewKeyBackup) {
614+
await this.resetKeyBackup();
615+
}
601616
}
602617
}
603618

619+
/**
620+
* Deletes the given key backup.
621+
*
622+
* @param version - The backup version to delete.
623+
*/
624+
public async deleteKeyBackupVersion(version: string): Promise<void> {
625+
await this.backupManager.deleteKeyBackupVersion(version);
626+
}
627+
604628
/**
605629
* Implementation of {@link CryptoApi#resetKeyBackup}.
606630
*/
607631
public async resetKeyBackup(): Promise<void> {
608-
// stub
632+
const backupInfo = await this.backupManager.setupKeyBackup((o) => this.signObject(o));
633+
634+
// we want to store the private key in 4S
635+
// need to check if 4S is set up?
636+
if (await this.secretStorageHasAESKey()) {
637+
await this.secretStorage.store("m.megolm_backup.v1", backupInfo.decryptionKey.toBase64());
638+
}
639+
640+
// we can check and start async
641+
this.checkKeyBackupAndEnable();
609642
}
610643

611-
public async deleteKeyBackupVersion(version: string): Promise<void> {
612-
// stub
644+
private async signObject<T extends ISignableObject & object>(obj: T): Promise<void> {
645+
const sigs = new Map(Object.entries(obj.signatures || {}));
646+
const unsigned = obj.unsigned;
647+
648+
delete obj.signatures;
649+
delete obj.unsigned;
650+
651+
const userSignatures = sigs.get(this.userId) || {};
652+
653+
const canonalizedJson = anotherjson.stringify(obj);
654+
const signatures: RustSdkCryptoJs.Signatures = await this.olmMachine.sign(canonalizedJson);
655+
656+
const map = JSON.parse(signatures.asJSON());
657+
658+
sigs.set(this.userId, { ...userSignatures, ...map[this.userId] });
659+
660+
if (unsigned !== undefined) obj.unsigned = unsigned;
661+
obj.signatures = Object.fromEntries(sigs.entries());
613662
}
663+
614664
/**
615665
* Add the secretStorage key to the secret storage
616666
* - The secret storage key must have the `keyInfo` field filled

0 commit comments

Comments
 (0)