1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-23 17:02:25 +03:00

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
This commit is contained in:
Valere
2023-09-05 15:52:49 +02:00
committed by GitHub
parent 989c5a3dda
commit d7831f9e5b
3 changed files with 185 additions and 45 deletions

View File

@@ -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(() => {
@@ -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
@@ -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;
}
@@ -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 });
@@ -2389,9 +2388,7 @@ 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 () => {
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 });
@@ -2421,14 +2418,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
// 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
@@ -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);
@@ -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();
});
});

View File

@@ -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
*/
@@ -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 =

View File

@@ -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";
@@ -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.
*
@@ -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
@@ -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();
}
}
}
@@ -938,18 +950,53 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
return await this.backupManager.checkKeyBackupAndEnable(true);
}
/**
* Implementation of {@link CryptoApi#resetKeyBackup}.
*/
public async resetKeyBackup(): Promise<void> {
// stub
}
/**
* Implementation of {@link CryptoApi#deleteKeyBackupVersion}.
*/
public async deleteKeyBackupVersion(version: string): Promise<void> {
// stub
await this.backupManager.deleteKeyBackupVersion(version);
}
/**
* Implementation of {@link CryptoApi#resetKeyBackup}.
*/
public async resetKeyBackup(): Promise<void> {
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();
}
/**
* 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
*/
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());
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////