You've already forked matrix-js-sdk
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:
@@ -68,7 +68,7 @@ import {
|
|||||||
mockSetupMegolmBackupRequests,
|
mockSetupMegolmBackupRequests,
|
||||||
} from "../../test-utils/mockEndpoints";
|
} from "../../test-utils/mockEndpoints";
|
||||||
import { AddSecretStorageKeyOpts, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
|
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";
|
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||||
|
|
||||||
afterEach(() => {
|
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.
|
* configure the account to have recovery.
|
||||||
*
|
*
|
||||||
* @param backupVersion - The version of the created backup
|
* @param backupVersion - The version of the created backup
|
||||||
@@ -2295,7 +2295,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
await bootstrapPromise;
|
await bootstrapPromise;
|
||||||
// Finally ensure backup is working
|
// Finally ensure backup is working
|
||||||
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
|
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
|
||||||
|
|
||||||
await backupStatusUpdate;
|
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
|
const bootstrapPromise = aliceClient
|
||||||
.getCrypto()!
|
.getCrypto()!
|
||||||
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
||||||
@@ -2389,9 +2388,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
newBackendOnly(
|
it("should create a new key if setupNewSecretStorage is at true even if an AES key is already in the secret storage", async () => {
|
||||||
"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
|
let bootstrapPromise = aliceClient
|
||||||
.getCrypto()!
|
.getCrypto()!
|
||||||
.bootstrapSecretStorage({ setupNewSecretStorage: true, createSecretStorageKey });
|
.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
|
// createSecretStorageKey should have been called twice, one time every bootstrapSecretStorage call
|
||||||
expect(createSecretStorageKey).toHaveBeenCalledTimes(2);
|
expect(createSecretStorageKey).toHaveBeenCalledTimes(2);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
newBackendOnly("should upload cross signing keys", async () => {
|
it("should upload cross signing keys", async () => {
|
||||||
mockSetupCrossSigningRequests();
|
mockSetupCrossSigningRequests();
|
||||||
|
|
||||||
// Before setting up secret-storage, bootstrap cross-signing, so that the client has cross-signing keys.
|
// 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.
|
// Now, when we bootstrap secret-storage, the cross-signing keys should be uploaded.
|
||||||
const bootstrapPromise = aliceClient
|
const bootstrapPromise = aliceClient
|
||||||
@@ -2457,16 +2453,24 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
expect(selfSigningKey[secretStorageKey]).toBeDefined();
|
expect(selfSigningKey[secretStorageKey]).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
oldBackendOnly("should create a new megolm backup", async () => {
|
it("should create a new megolm backup", async () => {
|
||||||
const backupVersion = "abc";
|
const backupVersion = "abc";
|
||||||
await bootstrapSecurity(backupVersion);
|
await bootstrapSecurity(backupVersion);
|
||||||
|
|
||||||
// Expect a backup to be available and used
|
// Expect a backup to be available and used
|
||||||
const activeBackup = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
|
const activeBackup = await aliceClient.getCrypto()!.getActiveSessionBackupVersion();
|
||||||
expect(activeBackup).toStrictEqual(backupVersion);
|
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
|
// First set up 4S and key backup
|
||||||
const backupVersion = "1";
|
const backupVersion = "1";
|
||||||
await bootstrapSecurity(backupVersion);
|
await bootstrapSecurity(backupVersion);
|
||||||
@@ -2539,10 +2543,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
expect(nextVersion).not.toEqual(currentVersion);
|
expect(nextVersion).not.toEqual(currentVersion);
|
||||||
expect(nextKey).not.toEqual(currentBackupKey);
|
expect(nextKey).not.toEqual(currentBackupKey);
|
||||||
|
|
||||||
// Test deletion of the backup
|
// The `deleteKeyBackupVersion` API is deprecated but has been modified to work with both crypto backend
|
||||||
await aliceClient.getCrypto()!.deleteKeyBackupVersion(nextVersion!);
|
// ensure that it works anyhow
|
||||||
|
await aliceClient.deleteKeyBackupVersion(nextVersion!);
|
||||||
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
|
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();
|
expect(await aliceClient.getCrypto()!.getActiveSessionBackupVersion()).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,9 +22,24 @@ import { logger } from "../logger";
|
|||||||
import { ClientPrefix, IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api";
|
import { ClientPrefix, IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api";
|
||||||
import { CryptoEvent } from "../crypto";
|
import { CryptoEvent } from "../crypto";
|
||||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||||
|
import { encodeUri } from "../utils";
|
||||||
import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
|
import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
|
||||||
import { sleep } from "../utils";
|
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
|
* @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 =
|
export type RustBackupCryptoEvents =
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import anotherjson from "another-json";
|
||||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||||
|
|
||||||
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
|
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
|
||||||
@@ -63,9 +64,15 @@ import { RustBackupCryptoEventMap, RustBackupCryptoEvents, RustBackupManager } f
|
|||||||
import { TypedReEmitter } from "../ReEmitter";
|
import { TypedReEmitter } from "../ReEmitter";
|
||||||
import { randomString } from "../randomstring";
|
import { randomString } from "../randomstring";
|
||||||
import { ClientStoppedError } from "../errors";
|
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"];
|
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.
|
* 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({
|
public async bootstrapSecretStorage({
|
||||||
createSecretStorageKey,
|
createSecretStorageKey,
|
||||||
setupNewSecretStorage,
|
setupNewSecretStorage,
|
||||||
|
setupNewKeyBackup,
|
||||||
}: CreateSecretStorageOpts = {}): Promise<void> {
|
}: CreateSecretStorageOpts = {}): Promise<void> {
|
||||||
// If an AES Key is already stored in the secret storage and setupNewSecretStorage is not set
|
// 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
|
// 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.master", crossSigningPrivateKeys.masterKey);
|
||||||
await this.secretStorage.store("m.cross_signing.user_signing", crossSigningPrivateKeys.userSigningKey);
|
await this.secretStorage.store("m.cross_signing.user_signing", crossSigningPrivateKeys.userSigningKey);
|
||||||
await this.secretStorage.store("m.cross_signing.self_signing", crossSigningPrivateKeys.self_signing_key);
|
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);
|
return await this.backupManager.checkKeyBackupAndEnable(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of {@link CryptoApi#resetKeyBackup}.
|
|
||||||
*/
|
|
||||||
public async resetKeyBackup(): Promise<void> {
|
|
||||||
// stub
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of {@link CryptoApi#deleteKeyBackupVersion}.
|
* Implementation of {@link CryptoApi#deleteKeyBackupVersion}.
|
||||||
*/
|
*/
|
||||||
public async deleteKeyBackupVersion(version: string): Promise<void> {
|
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
Reference in New Issue
Block a user