You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-31 15:24:23 +03:00
Save the key backup key to 4S during bootstrapCrossSigning
(#4542)
* Save the key backup key to secret storage When setting up secret storage, if we have a key backup key in cache (like we do for the cross signing secrets). * Add test * Get the key directly from the olmMachine saves converting it needlessly into a buffer to turn it back into a base64 string * Overwrite backup keyin storage if different * Fix test * Add integ test * Test failure case for sonar * Unused import * Missed return * Also check active backup version
This commit is contained in:
@ -3121,6 +3121,32 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
|||||||
const mskId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.Master)!;
|
const mskId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.Master)!;
|
||||||
expect(signatures![aliceClient.getUserId()!][`ed25519:${mskId}`]).toBeDefined();
|
expect(signatures![aliceClient.getUserId()!][`ed25519:${mskId}`]).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
newBackendOnly("should upload existing megolm backup key to a new 4S store", async () => {
|
||||||
|
const backupKeyTo4SPromise = awaitMegolmBackupKeyUpload();
|
||||||
|
|
||||||
|
// we need these to set up the mocks but we don't actually care whether they
|
||||||
|
// resolve because we're not testing those things in this test.
|
||||||
|
awaitCrossSigningKeyUpload("master");
|
||||||
|
awaitCrossSigningKeyUpload("user_signing");
|
||||||
|
awaitCrossSigningKeyUpload("self_signing");
|
||||||
|
awaitSecretStorageKeyStoredInAccountData();
|
||||||
|
|
||||||
|
mockSetupCrossSigningRequests();
|
||||||
|
mockSetupMegolmBackupRequests("1");
|
||||||
|
|
||||||
|
await aliceClient.getCrypto()!.bootstrapCrossSigning({});
|
||||||
|
await aliceClient.getCrypto()!.resetKeyBackup();
|
||||||
|
|
||||||
|
await aliceClient.getCrypto()!.bootstrapSecretStorage({
|
||||||
|
setupNewSecretStorage: true,
|
||||||
|
createSecretStorageKey,
|
||||||
|
setupNewKeyBackup: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await backupKeyTo4SPromise;
|
||||||
|
expect(accountDataAccumulator.accountDataEvents.get("m.megolm_backup.v1")).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Manage Key Backup", () => {
|
describe("Manage Key Backup", () => {
|
||||||
|
@ -727,6 +727,119 @@ describe("RustCrypto", () => {
|
|||||||
expect(resetKeyBackup.mock.calls).toHaveLength(2);
|
expect(resetKeyBackup.mock.calls).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("upload existing key backup key to new 4S store", () => {
|
||||||
|
const secretStorageCallbacks = {
|
||||||
|
getSecretStorageKey: async (keys: any, name: string) => {
|
||||||
|
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
|
||||||
|
},
|
||||||
|
} as SecretStorageCallbacks;
|
||||||
|
let secretStorage: ServerSideSecretStorageImpl;
|
||||||
|
|
||||||
|
let backupAuthData: any;
|
||||||
|
let backupAlg: string;
|
||||||
|
|
||||||
|
const fetchMock = {
|
||||||
|
authedRequest: jest.fn().mockImplementation((method, path, query, body) => {
|
||||||
|
if (path === "/room_keys/version") {
|
||||||
|
if (method === "POST") {
|
||||||
|
backupAuthData = body["auth_data"];
|
||||||
|
backupAlg = body["algorithm"];
|
||||||
|
return Promise.resolve({ version: "1", algorithm: backupAlg, auth_data: backupAuthData });
|
||||||
|
} else if (method === "GET" && backupAuthData) {
|
||||||
|
return Promise.resolve({ version: "1", algorithm: backupAlg, auth_data: backupAuthData });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve({});
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
backupAuthData = undefined;
|
||||||
|
backupAlg = "";
|
||||||
|
|
||||||
|
secretStorage = new ServerSideSecretStorageImpl(new DummyAccountDataClient(), secretStorageCallbacks);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("bootstrapSecretStorage saves megolm backup key if already cached", async () => {
|
||||||
|
const rustCrypto = await makeTestRustCrypto(
|
||||||
|
fetchMock as unknown as MatrixHttpApi<any>,
|
||||||
|
testData.TEST_USER_ID,
|
||||||
|
undefined,
|
||||||
|
secretStorage,
|
||||||
|
);
|
||||||
|
|
||||||
|
async function createSecretStorageKey() {
|
||||||
|
return {
|
||||||
|
keyInfo: {} as AddSecretStorageKeyOpts,
|
||||||
|
privateKey: new Uint8Array(32),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await rustCrypto.resetKeyBackup();
|
||||||
|
|
||||||
|
const storeSpy = jest.spyOn(secretStorage, "store");
|
||||||
|
|
||||||
|
await rustCrypto.bootstrapSecretStorage({
|
||||||
|
createSecretStorageKey,
|
||||||
|
setupNewSecretStorage: true,
|
||||||
|
setupNewKeyBackup: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(storeSpy).toHaveBeenCalledWith("m.megolm_backup.v1", expect.anything());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("bootstrapSecretStorage doesn't try to save megolm backup key not in cache", async () => {
|
||||||
|
const mockOlmMachine = {
|
||||||
|
isBackupEnabled: jest.fn().mockResolvedValue(false),
|
||||||
|
sign: jest.fn().mockResolvedValue({
|
||||||
|
asJSON: jest.fn().mockReturnValue("{}"),
|
||||||
|
}),
|
||||||
|
saveBackupDecryptionKey: jest.fn(),
|
||||||
|
crossSigningStatus: jest.fn().mockResolvedValue({
|
||||||
|
hasMaster: true,
|
||||||
|
hasSelfSigning: true,
|
||||||
|
hasUserSigning: true,
|
||||||
|
}),
|
||||||
|
exportCrossSigningKeys: jest.fn().mockResolvedValue({
|
||||||
|
masterKey: "sosecret",
|
||||||
|
userSigningKey: "secrets",
|
||||||
|
self_signing_key: "ssshhh",
|
||||||
|
}),
|
||||||
|
getBackupKeys: jest.fn().mockResolvedValue({}),
|
||||||
|
verifyBackup: jest.fn().mockResolvedValue({ trusted: jest.fn().mockReturnValue(false) }),
|
||||||
|
} as unknown as OlmMachine;
|
||||||
|
|
||||||
|
const rustCrypto = new RustCrypto(
|
||||||
|
logger,
|
||||||
|
mockOlmMachine,
|
||||||
|
fetchMock as unknown as MatrixHttpApi<any>,
|
||||||
|
TEST_USER,
|
||||||
|
TEST_DEVICE_ID,
|
||||||
|
secretStorage,
|
||||||
|
{} as CryptoCallbacks,
|
||||||
|
);
|
||||||
|
|
||||||
|
async function createSecretStorageKey() {
|
||||||
|
return {
|
||||||
|
keyInfo: {} as AddSecretStorageKeyOpts,
|
||||||
|
privateKey: new Uint8Array(32),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await rustCrypto.resetKeyBackup();
|
||||||
|
|
||||||
|
const storeSpy = jest.spyOn(secretStorage, "store");
|
||||||
|
|
||||||
|
await rustCrypto.bootstrapSecretStorage({
|
||||||
|
createSecretStorageKey,
|
||||||
|
setupNewSecretStorage: true,
|
||||||
|
setupNewKeyBackup: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(storeSpy).not.toHaveBeenCalledWith("m.megolm_backup.v1", expect.anything());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("isSecretStorageReady", async () => {
|
it("isSecretStorageReady", async () => {
|
||||||
const mockSecretStorage = {
|
const mockSecretStorage = {
|
||||||
getDefaultKeyId: jest.fn().mockResolvedValue(null),
|
getDefaultKeyId: jest.fn().mockResolvedValue(null),
|
||||||
|
@ -843,11 +843,53 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
|||||||
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) {
|
// likewise with the key backup key: if we have one, store it in secret storage (if it's not already there)
|
||||||
|
// also don't bother storing it if we're about to set up a new backup
|
||||||
|
if (!setupNewKeyBackup) {
|
||||||
|
await this.saveBackupKeyToStorage();
|
||||||
|
} else {
|
||||||
await this.resetKeyBackup();
|
await this.resetKeyBackup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we have a backup key for the current, trusted backup in cache,
|
||||||
|
* and we have secret storage active, save it to secret storage.
|
||||||
|
*/
|
||||||
|
private async saveBackupKeyToStorage(): Promise<void> {
|
||||||
|
const keyBackupInfo = await this.backupManager.getServerBackupInfo();
|
||||||
|
if (!keyBackupInfo || !keyBackupInfo.version) {
|
||||||
|
logger.info("Not saving backup key to secret storage: no backup info");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeBackupVersion = await this.backupManager.getActiveBackupVersion();
|
||||||
|
if (!activeBackupVersion || activeBackupVersion !== keyBackupInfo.version) {
|
||||||
|
logger.info("Not saving backup key to secret storage: backup keys do not match active backup version");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupKeys: RustSdkCryptoJs.BackupKeys = await this.olmMachine.getBackupKeys();
|
||||||
|
if (!backupKeys.decryptionKey) {
|
||||||
|
logger.info("Not saving backup key to secret storage: no backup key");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decryptionKeyMatchesKeyBackupInfo(backupKeys.decryptionKey, keyBackupInfo)) {
|
||||||
|
logger.info("Not saving backup key to secret storage: decryption key does not match backup info");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupKeyFromStorage = await this.secretStorage.get("m.megolm_backup.v1");
|
||||||
|
const backupKeyBase64 = backupKeys.decryptionKey.toBase64();
|
||||||
|
|
||||||
|
// The backup version that the key corresponds to isn't saved in 4S so if it's different, we must assume
|
||||||
|
// it's stale and overwrite.
|
||||||
|
if (backupKeyFromStorage !== backupKeyBase64) {
|
||||||
|
await this.secretStorage.store("m.megolm_backup.v1", backupKeyBase64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the secretStorage key to the secret storage
|
* Add the secretStorage key to the secret storage
|
||||||
* - The secret storage key must have the `keyInfo` field filled
|
* - The secret storage key must have the `keyInfo` field filled
|
||||||
|
Reference in New Issue
Block a user