Skip to content

Commit

Permalink
Implement key backup APIs for rust and create backup in `bootstrapSec…
Browse files Browse the repository at this point in the history
…retStorage` (#3690)

* new resetKeyBackup API

* add delete backup version test

* code review

* support backup creation in rust

* code review
  • Loading branch information
BillCarsonFr authored Sep 5, 2023
1 parent 989c5a3 commit d7831f9
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 41 deletions.
79 changes: 42 additions & 37 deletions spec/integ/crypto/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
});
});
Expand Down
88 changes: 88 additions & 0 deletions src/rust-crypto/backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -280,6 +295,79 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
}
}
}

/**
* Creates a new key backup by generating a new random private key.
*
* If there is an existing backup server side it will be deleted and replaced
* by the new one.
*
* @param signObject - Method that should sign the backup with existing device and
* existing identity.
* @returns a KeyBackupCreationInfo - All information related to the backup.
*/
public async setupKeyBackup(signObject: (authData: AuthData) => Promise<void>): Promise<KeyBackupCreationInfo> {
// 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<void> {
// 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<void> {
logger.debug(`deleteKeyBackupVersion v:${version}`);
const path = encodeUri("/room_keys/version/$version", { $version: version });
await this.http.authedRequest<void>(Method.Delete, path, undefined, undefined, {
prefix: ClientPrefix.V3,
});
}
}

export type RustBackupCryptoEvents =
Expand Down
55 changes: 51 additions & 4 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -555,6 +562,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
public async bootstrapSecretStorage({
createSecretStorageKey,
setupNewSecretStorage,
setupNewKeyBackup,
}: CreateSecretStorageOpts = {}): Promise<void> {
// 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
Expand Down Expand Up @@ -598,6 +606,10 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
await this.secretStorage.store("m.cross_signing.master", crossSigningPrivateKeys.masterKey);
await this.secretStorage.store("m.cross_signing.user_signing", crossSigningPrivateKeys.userSigningKey);
await this.secretStorage.store("m.cross_signing.self_signing", crossSigningPrivateKeys.self_signing_key);

if (setupNewKeyBackup) {
await this.resetKeyBackup();
}
}
}

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

/**
* Implementation of {@link CryptoApi#deleteKeyBackupVersion}.
*/
public async deleteKeyBackupVersion(version: string): Promise<void> {
await this.backupManager.deleteKeyBackupVersion(version);
}

/**
* Implementation of {@link CryptoApi#resetKeyBackup}.
*/
public async resetKeyBackup(): Promise<void> {
// stub
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();
}

/**
* Implementation of {@link CryptoApi#deleteKeyBackupVersion}.
* 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
*/
public async deleteKeyBackupVersion(version: string): Promise<void> {
// stub
private async signObject<T extends ISignableObject & object>(obj: T): Promise<void> {
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());
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down

0 comments on commit d7831f9

Please sign in to comment.