diff --git a/spec/integ/crypto/megolm-backup.spec.ts b/spec/integ/crypto/megolm-backup.spec.ts index 05b7f133d67..874a83f7d09 100644 --- a/spec/integ/crypto/megolm-backup.spec.ts +++ b/spec/integ/crypto/megolm-backup.spec.ts @@ -37,7 +37,13 @@ import { advanceTimersUntil, awaitDecryption, syncPromise } from "../../test-uti import * as testData from "../../test-utils/test-data"; import { type KeyBackupInfo, type KeyBackupSession } from "../../../src/crypto-api/keybackup"; import { flushPromises } from "../../test-utils/flushPromises"; -import { decodeRecoveryKey, DecryptionFailureCode, CryptoEvent, type CryptoApi } from "../../../src/crypto-api"; +import { + decodeRecoveryKey, + DecryptionFailureCode, + CryptoEvent, + type CryptoApi, + DecryptionKeyDoesNotMatchError, +} from "../../../src/crypto-api"; import { type KeyBackup } from "../../../src/rust-crypto/backup.ts"; const ROOM_ID = testData.TEST_ROOM_ID; @@ -502,15 +508,10 @@ describe("megolm-keys backup", () => { // @ts-ignore - mock a private method for testing purpose vi.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64); - const fullBackup = { - rooms: { - [ROOM_ID]: { - sessions: { - [testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA, - }, - }, - }, - }; + const fullBackup = createFullBackup( + testData.MEGOLM_SESSION_DATA.session_id, + testData.CURVE25519_KEY_BACKUP_DATA, + ); fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup); await aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage(); @@ -521,9 +522,38 @@ describe("megolm-keys backup", () => { expect(result.imported).toStrictEqual(1); }); + it("Should throw an error if the decryption key does not match the backup", async function () { + // Given the stored backup decryption key does not match the public backup info + // @ts-ignore - mock a private method for testing purpose + vi.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64_ALT); + + const fullBackup = createFullBackup( + testData.MEGOLM_SESSION_DATA.session_id, + testData.CURVE25519_KEY_BACKUP_DATA, + ); + fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup); + + // When we load that key, we throw because the keys don't match + await expect(aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage()).rejects.toThrow( + DecryptionKeyDoesNotMatchError, + ); + }); + it("Should throw an error if the decryption key is not found in cache", async () => { await expect(aliceCrypto.restoreKeyBackup()).rejects.toThrow("No decryption key found in crypto store"); }); + + function createFullBackup(sessionId: string, data: KeyBackupSession) { + return { + rooms: { + [ROOM_ID]: { + sessions: { + [sessionId]: data, + }, + }, + }, + }; + } }); describe("backupLoop", () => { diff --git a/spec/test-utils/test-data/index.ts b/spec/test-utils/test-data/index.ts index c1aff8103b5..f472ee5c0e0 100644 --- a/spec/test-utils/test-data/index.ts +++ b/spec/test-utils/test-data/index.ts @@ -212,9 +212,12 @@ export const ENCRYPTED_EVENT: Partial = { "origin_server_ts": 1507753886000 }; -/** base64-encoded backup decryption (private) key */ +/** base64-encoded backup decryption (private) key that matches the public key in CURVE25519_KEY_BACKUP_DATA */ export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo="; +/** base64-encoded backup decryption (private) key that does not match the public key in CURVE25519_KEY_BACKUP_DATA */ +export const BACKUP_DECRYPTION_KEY_BASE64_ALT = "dh4fP2LITyJusgnb0dEq/SQK253WGObvLxXF5FEX6qc"; + /** Backup decryption key in export format */ export const BACKUP_DECRYPTION_KEY_BASE58 = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d"; @@ -232,7 +235,7 @@ export const SIGNED_BACKUP_DATA: KeyBackupInfo = { } }; -/** +/** * Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}`. * Contains the key from MEGOLM_SESSION_DATA. */ @@ -463,7 +466,7 @@ export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = { } }; -/** +/** * Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}`. * Contains the key from BOB_MEGOLM_SESSION_DATA. */ diff --git a/src/crypto-api/index.ts b/src/crypto-api/index.ts index b4c41470fa2..be47c751973 100644 --- a/src/crypto-api/index.ts +++ b/src/crypto-api/index.ts @@ -563,8 +563,11 @@ export interface CryptoApi { * if they match, stores the key in the crypto store by calling {@link storeSessionBackupPrivateKey}, * which enables automatic restore of individual keys when an Unable-to-decrypt error is encountered. * - * If we are unable to fetch the key from secret storage, there is no backup on the server, or the key - * does not match, throws an exception. + * If the backup decryption key from secret storage does not match the + * latest backup on the server, we throw a {@link DecryptionKeyDoesNotMatchError}. + * + * If we are unable to fetch the key from secret storage or there is no backup on the server, + * we throw an exception. */ loadSessionBackupPrivateKeyFromSecretStorage(): Promise; @@ -1339,6 +1342,18 @@ export interface OlmEncryptionInfo { senderVerified: boolean; } +/** + * An error thrown by loadSessionBackupPrivateKeyFromSecretStorage indicating + * that the decryption key found in secret storage does not match the public key + * of the latest backup. + */ +export class DecryptionKeyDoesNotMatchError extends Error { + public constructor(message: string) { + super(message); + this.name = "DecryptionKeyDoesNotMatchError"; + } +} + export * from "./verification.ts"; export type * from "./keybackup.ts"; export * from "./recovery-key.ts"; diff --git a/src/rust-crypto/rust-crypto.ts b/src/rust-crypto/rust-crypto.ts index 5f3cca76292..2ffa8e9b439 100644 --- a/src/rust-crypto/rust-crypto.ts +++ b/src/rust-crypto/rust-crypto.ts @@ -50,6 +50,7 @@ import { CryptoEvent, type CryptoEventHandlerMap, DecryptionFailureCode, + DecryptionKeyDoesNotMatchError, deriveRecoveryKeyFromPassphrase, type DeviceIsolationMode, DeviceIsolationModeKind, @@ -1315,7 +1316,9 @@ export class RustCrypto extends TypedEventEmitter