Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement key backup APIs for rust and create backup in bootstrapSecretStorage #3690

Merged
merged 7 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading