Skip to content

Commit a0502c5

Browse files
authored
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
1 parent d1de32e commit a0502c5

3 files changed

Lines changed: 182 additions & 1 deletion

File tree

spec/integ/crypto/crypto.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3121,6 +3121,32 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
31213121
const mskId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.Master)!;
31223122
expect(signatures![aliceClient.getUserId()!][`ed25519:${mskId}`]).toBeDefined();
31233123
});
3124+
3125+
newBackendOnly("should upload existing megolm backup key to a new 4S store", async () => {
3126+
const backupKeyTo4SPromise = awaitMegolmBackupKeyUpload();
3127+
3128+
// we need these to set up the mocks but we don't actually care whether they
3129+
// resolve because we're not testing those things in this test.
3130+
awaitCrossSigningKeyUpload("master");
3131+
awaitCrossSigningKeyUpload("user_signing");
3132+
awaitCrossSigningKeyUpload("self_signing");
3133+
awaitSecretStorageKeyStoredInAccountData();
3134+
3135+
mockSetupCrossSigningRequests();
3136+
mockSetupMegolmBackupRequests("1");
3137+
3138+
await aliceClient.getCrypto()!.bootstrapCrossSigning({});
3139+
await aliceClient.getCrypto()!.resetKeyBackup();
3140+
3141+
await aliceClient.getCrypto()!.bootstrapSecretStorage({
3142+
setupNewSecretStorage: true,
3143+
createSecretStorageKey,
3144+
setupNewKeyBackup: false,
3145+
});
3146+
3147+
await backupKeyTo4SPromise;
3148+
expect(accountDataAccumulator.accountDataEvents.get("m.megolm_backup.v1")).toBeDefined();
3149+
});
31243150
});
31253151

31263152
describe("Manage Key Backup", () => {

spec/unit/rust-crypto/rust-crypto.spec.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,119 @@ describe("RustCrypto", () => {
727727
expect(resetKeyBackup.mock.calls).toHaveLength(2);
728728
});
729729

730+
describe("upload existing key backup key to new 4S store", () => {
731+
const secretStorageCallbacks = {
732+
getSecretStorageKey: async (keys: any, name: string) => {
733+
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
734+
},
735+
} as SecretStorageCallbacks;
736+
let secretStorage: ServerSideSecretStorageImpl;
737+
738+
let backupAuthData: any;
739+
let backupAlg: string;
740+
741+
const fetchMock = {
742+
authedRequest: jest.fn().mockImplementation((method, path, query, body) => {
743+
if (path === "/room_keys/version") {
744+
if (method === "POST") {
745+
backupAuthData = body["auth_data"];
746+
backupAlg = body["algorithm"];
747+
return Promise.resolve({ version: "1", algorithm: backupAlg, auth_data: backupAuthData });
748+
} else if (method === "GET" && backupAuthData) {
749+
return Promise.resolve({ version: "1", algorithm: backupAlg, auth_data: backupAuthData });
750+
}
751+
}
752+
return Promise.resolve({});
753+
}),
754+
};
755+
756+
beforeEach(() => {
757+
backupAuthData = undefined;
758+
backupAlg = "";
759+
760+
secretStorage = new ServerSideSecretStorageImpl(new DummyAccountDataClient(), secretStorageCallbacks);
761+
});
762+
763+
it("bootstrapSecretStorage saves megolm backup key if already cached", async () => {
764+
const rustCrypto = await makeTestRustCrypto(
765+
fetchMock as unknown as MatrixHttpApi<any>,
766+
testData.TEST_USER_ID,
767+
undefined,
768+
secretStorage,
769+
);
770+
771+
async function createSecretStorageKey() {
772+
return {
773+
keyInfo: {} as AddSecretStorageKeyOpts,
774+
privateKey: new Uint8Array(32),
775+
};
776+
}
777+
778+
await rustCrypto.resetKeyBackup();
779+
780+
const storeSpy = jest.spyOn(secretStorage, "store");
781+
782+
await rustCrypto.bootstrapSecretStorage({
783+
createSecretStorageKey,
784+
setupNewSecretStorage: true,
785+
setupNewKeyBackup: false,
786+
});
787+
788+
expect(storeSpy).toHaveBeenCalledWith("m.megolm_backup.v1", expect.anything());
789+
});
790+
791+
it("bootstrapSecretStorage doesn't try to save megolm backup key not in cache", async () => {
792+
const mockOlmMachine = {
793+
isBackupEnabled: jest.fn().mockResolvedValue(false),
794+
sign: jest.fn().mockResolvedValue({
795+
asJSON: jest.fn().mockReturnValue("{}"),
796+
}),
797+
saveBackupDecryptionKey: jest.fn(),
798+
crossSigningStatus: jest.fn().mockResolvedValue({
799+
hasMaster: true,
800+
hasSelfSigning: true,
801+
hasUserSigning: true,
802+
}),
803+
exportCrossSigningKeys: jest.fn().mockResolvedValue({
804+
masterKey: "sosecret",
805+
userSigningKey: "secrets",
806+
self_signing_key: "ssshhh",
807+
}),
808+
getBackupKeys: jest.fn().mockResolvedValue({}),
809+
verifyBackup: jest.fn().mockResolvedValue({ trusted: jest.fn().mockReturnValue(false) }),
810+
} as unknown as OlmMachine;
811+
812+
const rustCrypto = new RustCrypto(
813+
logger,
814+
mockOlmMachine,
815+
fetchMock as unknown as MatrixHttpApi<any>,
816+
TEST_USER,
817+
TEST_DEVICE_ID,
818+
secretStorage,
819+
{} as CryptoCallbacks,
820+
);
821+
822+
async function createSecretStorageKey() {
823+
return {
824+
keyInfo: {} as AddSecretStorageKeyOpts,
825+
privateKey: new Uint8Array(32),
826+
};
827+
}
828+
829+
await rustCrypto.resetKeyBackup();
830+
831+
const storeSpy = jest.spyOn(secretStorage, "store");
832+
833+
await rustCrypto.bootstrapSecretStorage({
834+
createSecretStorageKey,
835+
setupNewSecretStorage: true,
836+
setupNewKeyBackup: false,
837+
});
838+
839+
expect(storeSpy).not.toHaveBeenCalledWith("m.megolm_backup.v1", expect.anything());
840+
});
841+
});
842+
730843
it("isSecretStorageReady", async () => {
731844
const mockSecretStorage = {
732845
getDefaultKeyId: jest.fn().mockResolvedValue(null),

src/rust-crypto/rust-crypto.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -843,11 +843,53 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
843843
await this.secretStorage.store("m.cross_signing.self_signing", crossSigningPrivateKeys.self_signing_key);
844844
}
845845

846-
if (setupNewKeyBackup) {
846+
// likewise with the key backup key: if we have one, store it in secret storage (if it's not already there)
847+
// also don't bother storing it if we're about to set up a new backup
848+
if (!setupNewKeyBackup) {
849+
await this.saveBackupKeyToStorage();
850+
} else {
847851
await this.resetKeyBackup();
848852
}
849853
}
850854

855+
/**
856+
* If we have a backup key for the current, trusted backup in cache,
857+
* and we have secret storage active, save it to secret storage.
858+
*/
859+
private async saveBackupKeyToStorage(): Promise<void> {
860+
const keyBackupInfo = await this.backupManager.getServerBackupInfo();
861+
if (!keyBackupInfo || !keyBackupInfo.version) {
862+
logger.info("Not saving backup key to secret storage: no backup info");
863+
return;
864+
}
865+
866+
const activeBackupVersion = await this.backupManager.getActiveBackupVersion();
867+
if (!activeBackupVersion || activeBackupVersion !== keyBackupInfo.version) {
868+
logger.info("Not saving backup key to secret storage: backup keys do not match active backup version");
869+
return;
870+
}
871+
872+
const backupKeys: RustSdkCryptoJs.BackupKeys = await this.olmMachine.getBackupKeys();
873+
if (!backupKeys.decryptionKey) {
874+
logger.info("Not saving backup key to secret storage: no backup key");
875+
return;
876+
}
877+
878+
if (!decryptionKeyMatchesKeyBackupInfo(backupKeys.decryptionKey, keyBackupInfo)) {
879+
logger.info("Not saving backup key to secret storage: decryption key does not match backup info");
880+
return;
881+
}
882+
883+
const backupKeyFromStorage = await this.secretStorage.get("m.megolm_backup.v1");
884+
const backupKeyBase64 = backupKeys.decryptionKey.toBase64();
885+
886+
// The backup version that the key corresponds to isn't saved in 4S so if it's different, we must assume
887+
// it's stale and overwrite.
888+
if (backupKeyFromStorage !== backupKeyBase64) {
889+
await this.secretStorage.store("m.megolm_backup.v1", backupKeyBase64);
890+
}
891+
}
892+
851893
/**
852894
* Add the secretStorage key to the secret storage
853895
* - The secret storage key must have the `keyInfo` field filled

0 commit comments

Comments
 (0)