Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save the key backup key to 4S during bootstrapCrossSigning #4542

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
Open
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 @@ -705,6 +705,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
37 changes: 36 additions & 1 deletion src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -841,11 +841,46 @@ 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 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");
}

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
Loading