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

Validate backup private key before migrating it #4114

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2a5e165
Migrate own identity trust to rust crypto
BillCarsonFr Mar 4, 2024
eebd5e1
Fix gendoc not happy if msk of IDownloadKeyResult has a signature
BillCarsonFr Mar 4, 2024
4c76d71
add missing mock
BillCarsonFr Mar 4, 2024
f24f67b
code review
BillCarsonFr Mar 6, 2024
6f389f5
Code review
BillCarsonFr Mar 6, 2024
027b025
Merge branch 'develop' into valere/element-r/migrate_local_trust_if_n…
BillCarsonFr Mar 14, 2024
bff57ad
Review gh suggestion
BillCarsonFr Mar 14, 2024
7e95829
Review gh suggestion
BillCarsonFr Mar 14, 2024
0dd4ef6
Review gh suggestion
BillCarsonFr Mar 14, 2024
230e79a
Review gh suggestion
BillCarsonFr Mar 14, 2024
0695d44
review move function down in file
BillCarsonFr Mar 14, 2024
6dfc534
Merge branch 'valere/element-r/migrate_local_trust_if_needed' of http…
BillCarsonFr Mar 14, 2024
c5fffba
Review gh suggestion
BillCarsonFr Mar 14, 2024
7965819
Review gh suggestion
BillCarsonFr Mar 14, 2024
7345204
Review: Cleaning tests, renaming
BillCarsonFr Mar 14, 2024
82a69c6
Review: better comment
BillCarsonFr Mar 14, 2024
a008231
Comment paragraphs
BillCarsonFr Mar 14, 2024
e034bc7
retry until initial key query is successfull
BillCarsonFr Mar 15, 2024
bf3e6d6
Validate backup private key before migrating it
BillCarsonFr Mar 15, 2024
56b8277
Merge branch 'develop' into valere/element-r/validate_backup_key_befo…
BillCarsonFr Apr 11, 2024
7c8ae90
post merge fix
BillCarsonFr Apr 11, 2024
df2b556
Fix test, missing mock
BillCarsonFr Apr 11, 2024
2cb89af
Use crypto wasm instead of lib olm to check backup key
BillCarsonFr Apr 11, 2024
6852682
typo
BillCarsonFr Apr 11, 2024
bc8e4a8
code review
BillCarsonFr Apr 12, 2024
28f7d12
quick lint
BillCarsonFr Apr 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
104 changes: 104 additions & 0 deletions spec/integ/crypto/rust-crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,110 @@ describe("MatrixClient.initRustCrypto", () => {
expect(progressListener).toHaveBeenLastCalledWith(-1, -1);
}, 60000);

describe("Private key backup migration", () => {
it("should not migrate the backup private key if backup has changed", async () => {
// Here we have a new backup server side, and the migrated account has the previous backup key.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.newBackupResponse);

fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);

await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");

const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});

await matrixClient.initRustCrypto();

const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
expect(privateBackupKey).toBeNull();
});

it("should not migrate the backup private key if backup has unknown algorithm", async () => {
// Here we have a new backup server side, and the migrated account has the previous backup key.
const backupResponse = {
...MSK_NOT_CACHED_DATASET.backupResponse,
algorithm: "m.megolm_backup.v8",
};
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupResponse);

fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);

await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");

const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});

await matrixClient.initRustCrypto();

const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
expect(privateBackupKey).toBeNull();
});

it("should not migrate the backup private key if the backup has been deleted", async () => {
// The old backup has been deleted server side.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No backup found",
},
});

fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);

await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");

const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});

await matrixClient.initRustCrypto();

const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
expect(privateBackupKey).toBeNull();
});

it("should migrate the backup private key if the backup matches", async () => {
// The old backup has been deleted server side.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.backupResponse);

fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);

await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");

const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});

await matrixClient.initRustCrypto();

const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
expect(privateBackupKey).toBeDefined();
});
});

describe("Legacy trust migration", () => {
async function populateAndStartLegacyCryptoStore(dumpPath: string): Promise<IndexedDBCryptoStore> {
const testStoreName = "test-store";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,28 @@ const BACKUP_RESPONSE: KeyBackupInfo = {
count: 0,
};

/**
* This was generated by doing a backup reset on the account.
* This is a new valid backup for this account.
*/
const NEW_BACKUP_RESPONSE: KeyBackupInfo = {
auth_data: {
public_key: "CkDxWALi3lcChgjEZFEM6clYq5x768XBwsL++eaOzTI",
signatures: {
"@migration:localhost": {
"ed25519:YVEGEYPYWX":
"ZSYuQDdwgB9WKXQ+z5aWWfqSolBCGRw53kur1Vy956gFefgzCBkMbw5M0I2UgfU2Cukri7jZ4ig201zmLNmaAA",
"ed25519:rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack":
"+UQ8EA507LoIqgK9rPsqPoGrj+iRBJeY2Oz0mMtXmVf8c1y8G0KWJNUWqvOysnOhsoJf1bt8ey48CxjjtSQ2AA",
},
},
},
version: "3",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
etag: "0",
count: 0,
};
BillCarsonFr marked this conversation as resolved.
Show resolved Hide resolved

/**
BillCarsonFr marked this conversation as resolved.
Show resolved Hide resolved
* A dataset containing the information for the tested user.
* To be used during tests.
Expand All @@ -256,5 +278,6 @@ export const MSK_NOT_CACHED_DATASET: DumpDataSetInfo = {
keyQueryResponse: KEY_QUERY_RESPONSE,
rotatedKeyQueryResponse: ROTATED_KEY_QUERY_RESPONSE,
backupResponse: BACKUP_RESPONSE,
newBackupResponse: NEW_BACKUP_RESPONSE,
BillCarsonFr marked this conversation as resolved.
Show resolved Hide resolved
dumpPath: "spec/test-utils/test_indexeddb_cryptostore_dump/no_cached_msk_dump/dump.json",
};
15 changes: 14 additions & 1 deletion spec/unit/rust-crypto/rust-crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,20 @@ describe("initRustCrypto", () => {
createMegolmSessions(legacyStore, nDevices, nSessionsPerDevice);
await legacyStore.markSessionsNeedingBackup([{ senderKey: pad43("device5"), sessionId: "session5" }]);

fetchMock.get("path:/_matrix/client/v3/room_keys/version", { version: "45" });
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
auth_data: {
public_key: "backup_key_public",
},
version: "45",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
});
// The cached key should be valid for the backup
const mockBackupDecryptionKey: any = {
megolmV1PublicKey: {
publicKeyBase64: "backup_key_public",
},
};
jest.spyOn(RustSdkCryptoJs.BackupDecryptionKey, "fromBase64").mockReturnValue(mockBackupDecryptionKey);

function legacyMigrationProgressListener(progress: number, total: number): void {
logger.log(`migrated ${progress} of ${total}`);
Expand Down
28 changes: 23 additions & 5 deletions src/rust-crypto/libolm_migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { decryptAES, IEncryptedPayload } from "../crypto/aes";
import { IHttpOpts, MatrixHttpApi } from "../http-api";
import { requestKeyBackupVersion } from "./backup";
import { IRoomEncryption } from "../crypto/RoomList";
import { CrossSigningKeyInfo } from "../crypto-api";
import { CrossSigningKeyInfo, Curve25519AuthData } from "../crypto-api";
import { RustCrypto } from "./rust-crypto";
import { KeyBackupInfo } from "../crypto-api/keybackup";
import { sleep } from "../utils";
Expand Down Expand Up @@ -161,7 +161,8 @@ async function migrateBaseData(
const recoveryKey = await getAndDecryptCachedSecretKey(legacyStore, pickleKey, "m.megolm_backup.v1");

// If we have a backup recovery key, we need to try to figure out which backup version it is for.
// All we can really do is ask the server for the most recent version.
// All we can really do is ask the server for the most recent version and check if the cached key we have matches.
// It is possible that the backup has changed since last time his session was opened.
if (recoveryKey) {
let backupCallDone = false;
let backupInfo: KeyBackupInfo | null = null;
Expand All @@ -175,9 +176,26 @@ async function migrateBaseData(
await sleep(2000);
}
}
if (backupInfo) {
migrationData.backupVersion = backupInfo.version;
migrationData.backupRecoveryKey = recoveryKey;
if (backupInfo && backupInfo.algorithm == "m.megolm_backup.v1.curve25519-aes-sha2") {
BillCarsonFr marked this conversation as resolved.
Show resolved Hide resolved
// check if the recovery key matches, as the active backup version may have changed since the key was cached
// and the migration started.
try {
const decryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(recoveryKey);
const publicKey = (backupInfo.auth_data as Curve25519AuthData)?.public_key;
const isValid = decryptionKey.megolmV1PublicKey.publicKeyBase64 == publicKey;
if (isValid) {
migrationData.backupVersion = backupInfo.version;
migrationData.backupRecoveryKey = recoveryKey;
} else {
logger.debug(
"The backup key to migrate does not match the active backup version",
`Cached pub key: ${decryptionKey.megolmV1PublicKey.publicKeyBase64}`,
`Active pub key: ${publicKey}`,
);
}
BillCarsonFr marked this conversation as resolved.
Show resolved Hide resolved
} catch (e) {
logger.warn("Failed to check if the backup key to migrate matches the active backup version", e);
}
}
}

Expand Down