Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 40 additions & 10 deletions spec/integ/crypto/megolm-backup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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", () => {
Expand Down
9 changes: 6 additions & 3 deletions spec/test-utils/test-data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,12 @@ export const ENCRYPTED_EVENT: Partial<IEvent> = {
"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";

Expand All @@ -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.
*/
Expand Down Expand Up @@ -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.
*/
Expand Down
19 changes: 17 additions & 2 deletions src/crypto-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;

Expand Down Expand Up @@ -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";
Expand Down
5 changes: 4 additions & 1 deletion src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
CryptoEvent,
type CryptoEventHandlerMap,
DecryptionFailureCode,
DecryptionKeyDoesNotMatchError,
deriveRecoveryKeyFromPassphrase,
type DeviceIsolationMode,
DeviceIsolationModeKind,
Expand Down Expand Up @@ -1315,7 +1316,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH

const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(backupKey);
if (!decryptionKeyMatchesKeyBackupInfo(backupDecryptionKey, keyBackupInfo)) {
throw new Error("loadSessionBackupPrivateKeyFromSecretStorage: decryption key does not match backup info");
throw new DecryptionKeyDoesNotMatchError(
"loadSessionBackupPrivateKeyFromSecretStorage: decryption key does not match backup info",
);
}

await this.backupManager.saveBackupDecryptionKey(backupDecryptionKey, keyBackupInfo.version);
Expand Down
Loading