Skip to content

Commit dc7a029

Browse files
committed
support backup creation in rust
1 parent 28a85fd commit dc7a029

File tree

3 files changed

+207
-39
lines changed

3 files changed

+207
-39
lines changed

spec/integ/crypto/crypto.spec.ts

Lines changed: 41 additions & 35 deletions
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 },
@@ -2295,7 +2297,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
22952297
await bootstrapPromise;
22962298
// Finally ensure backup is working
22972299
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
2298-
22992300
await backupStatusUpdate;
23002301
}
23012302

@@ -2346,7 +2347,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
23462347
},
23472348
);
23482349

2349-
newBackendOnly("should create a new key", async () => {
2350+
it("should create a new key", async () => {
23502351
const bootstrapPromise = aliceClient
23512352
.getCrypto()!
23522353
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
@@ -2389,46 +2390,43 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
23892390
},
23902391
);
23912392

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 });
2393+
it("should create a new key if setupNewSecretStorage is at true even if an AES key is already in the secret storage", async () => {
2394+
let bootstrapPromise = aliceClient
2395+
.getCrypto()!
2396+
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
23982397

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

2402-
// Return the newly created key in the sync response
2403-
sendSyncResponse(secretStorageKey);
2401+
// Return the newly created key in the sync response
2402+
sendSyncResponse(secretStorageKey);
24042403

2405-
// Wait for bootstrapSecretStorage to finished
2406-
await bootstrapPromise;
2404+
// Wait for bootstrapSecretStorage to finished
2405+
await bootstrapPromise;
24072406

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

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

2416-
// Return the newly created key in the sync response
2417-
sendSyncResponse(secretStorageKey);
2415+
// Return the newly created key in the sync response
2416+
sendSyncResponse(secretStorageKey);
24182417

2419-
// Wait for bootstrapSecretStorage to finished
2420-
await bootstrapPromise;
2418+
// Wait for bootstrapSecretStorage to finished
2419+
await bootstrapPromise;
24212420

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

2427-
newBackendOnly("should upload cross signing keys", async () => {
2425+
it("should upload cross signing keys", async () => {
24282426
mockSetupCrossSigningRequests();
24292427

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

24332431
// Now, when we bootstrap secret-storage, the cross-signing keys should be uploaded.
24342432
const bootstrapPromise = aliceClient
@@ -2457,17 +2455,25 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
24572455
expect(selfSigningKey[secretStorageKey]).toBeDefined();
24582456
});
24592457

2460-
oldBackendOnly("should create a new megolm backup", async () => {
2458+
it("should create a new megolm backup", async () => {
24612459
const backupVersion = "abc";
24622460
await bootstrapSecurity(backupVersion);
24632461

24642462
// Expect a backup to be available and used
24652463
const activeBackup = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
24662464
expect(activeBackup).toStrictEqual(backupVersion);
2465+
2466+
// check that there is a MSK signature
2467+
const signatures = (await aliceClient.getCrypto()!.checkKeyBackupAndEnable())!.backupInfo.auth_data!
2468+
.signatures;
2469+
expect(signatures).toBeDefined();
2470+
expect(signatures![aliceClient.getUserId()!]).toBeDefined();
2471+
const mskId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.Master)!;
2472+
expect(signatures![aliceClient.getUserId()!][`ed25519:${mskId}`]).toBeDefined();
24672473
});
24682474

2469-
oldBackendOnly("Reset key backup should create a new backup and update 4S", async () => {
2470-
// First set up 4S and key backup
2475+
it("Reset key backup should create a new backup and update 4S", async () => {
2476+
// First set up recovery
24712477
const backupVersion = "1";
24722478
await bootstrapSecurity(backupVersion);
24732479

src/rust-crypto/backup.ts

Lines changed: 112 additions & 0 deletions
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

Lines changed: 54 additions & 4 deletions
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,6 +609,10 @@ 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

@@ -938,18 +953,53 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
938953
return await this.backupManager.checkKeyBackupAndEnable(true);
939954
}
940955

956+
/**
957+
* Implementation of {@link CryptoApi#deleteKeyBackupVersion}.
958+
*/
959+
public async deleteKeyBackupVersion(version: string): Promise<void> {
960+
await this.backupManager.deleteKeyBackupVersion(version);
961+
}
962+
941963
/**
942964
* Implementation of {@link CryptoApi#resetKeyBackup}.
943965
*/
944966
public async resetKeyBackup(): Promise<void> {
945-
// stub
967+
const backupInfo = await this.backupManager.setupKeyBackup((o) => this.signObject(o));
968+
969+
// we want to store the private key in 4S
970+
// need to check if 4S is set up?
971+
if (await this.secretStorageHasAESKey()) {
972+
await this.secretStorage.store("m.megolm_backup.v1", backupInfo.decryptionKey.toBase64());
973+
}
974+
975+
// we can check and start async
976+
this.checkKeyBackupAndEnable();
946977
}
947978

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

9551005
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

0 commit comments

Comments
 (0)