Skip to content

Commit

Permalink
Save the key backup key to 4S during bootstrapCrossSigning (#4542)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
dbkr authored Dec 12, 2024
1 parent d1de32e commit a0502c5
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 1 deletion.
26 changes: 26 additions & 0 deletions spec/integ/crypto/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3121,6 +3121,32 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const mskId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.Master)!;
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", () => {
Expand Down
113 changes: 113 additions & 0 deletions spec/unit/rust-crypto/rust-crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,119 @@ describe("RustCrypto", () => {
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 () => {
const mockSecretStorage = {
getDefaultKeyId: jest.fn().mockResolvedValue(null),
Expand Down
44 changes: 43 additions & 1 deletion src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -843,11 +843,53 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
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();
}
}

/**
* 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
* - The secret storage key must have the `keyInfo` field filled
Expand Down

0 comments on commit a0502c5

Please sign in to comment.