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

Rust crypto: ensure we persist the key backup version #3770

Merged
merged 9 commits into from
Oct 4, 2023
28 changes: 23 additions & 5 deletions spec/integ/crypto/megolm-backup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,31 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
},
};

fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
testData.CURVE25519_KEY_BACKUP_DATA,
);
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", (url, request) => {
// check that the version is correct
const version = new URLSearchParams(new URL(url).search).get("version");
if (version == "1") {
return testData.CURVE25519_KEY_BACKUP_DATA;
} else {
return {
status: 403,
body: {
current_version: "1",
errcode: "M_WRONG_ROOM_KEYS_VERSION",
error: "Wrong backup version.",
},
};
}
});

fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);

aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceCrypto.storeSessionBackupPrivateKey(Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"));
await aliceCrypto.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
testData.SIGNED_BACKUP_DATA.version!,
);

// start after saving the private key
await aliceClient.startClient();
Expand Down Expand Up @@ -645,6 +661,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
await aliceClient.startClient();
await aliceCrypto.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
testData.SIGNED_BACKUP_DATA.version!,
);

const result = await aliceCrypto.isKeyBackupTrusted(testData.SIGNED_BACKUP_DATA);
Expand All @@ -658,6 +675,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
await aliceClient.startClient();
await aliceCrypto.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
testData.SIGNED_BACKUP_DATA.version!,
);

const backup: KeyBackupInfo = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
Expand Down
4 changes: 2 additions & 2 deletions spec/unit/crypto/backup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ describe("MegolmBackup", function () {
return client
.initCrypto()
.then(() => {
return client.crypto!.storeSessionBackupPrivateKey(new Uint8Array(32));
return client.crypto!.storeSessionBackupPrivateKey(new Uint8Array(32), "1");
})
.then(() => {
return cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
Expand Down Expand Up @@ -696,7 +696,7 @@ describe("MegolmBackup", function () {

it("has working cache functions", async function () {
const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
await client.crypto!.storeSessionBackupPrivateKey(key);
await client.crypto!.storeSessionBackupPrivateKey(key, "1");
const result = await client.crypto!.getSessionBackupPrivateKey();
expect(new Uint8Array(result!)).toEqual(key);
});
Expand Down
5 changes: 4 additions & 1 deletion spec/unit/rust-crypto/rust-crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,10 @@ describe("RustCrypto", () => {
it("can save and restore a key", async () => {
const key = "testtesttesttesttesttesttesttest";
const rustCrypto = await makeTestRustCrypto();
await rustCrypto.storeSessionBackupPrivateKey(new TextEncoder().encode(key));
await rustCrypto.storeSessionBackupPrivateKey(
new TextEncoder().encode(key),
testData.SIGNED_BACKUP_DATA.version!,
);
const fetched = await rustCrypto.getSessionBackupPrivateKey();
expect(new TextDecoder().decode(fetched!)).toEqual(key);
});
Expand Down
14 changes: 8 additions & 6 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3880,12 +3880,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
// Cache the key, if possible.
// This is async.
this.cryptoBackend
.storeSessionBackupPrivateKey(privKey)
.catch((e) => {
logger.warn("Error caching session backup key:", e);
})
.then(cacheCompleteCallback);
if (backupInfo.version != null) {
richvdh marked this conversation as resolved.
Show resolved Hide resolved
this.cryptoBackend
.storeSessionBackupPrivateKey(privKey, backupInfo.version)
.catch((e) => {
logger.warn("Error caching session backup key:", e);
})
.then(cacheCompleteCallback);
}

if (progressCallback) {
progressCallback({
Expand Down
5 changes: 4 additions & 1 deletion src/crypto-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,10 +372,13 @@ export interface CryptoApi {
* Store the backup decryption key.
*
* This should be called if the client has received the key from another device via secret sharing (gossiping).
* It is the responsability of the caller to check that the decryption key is valid to the given backup version.
richvdh marked this conversation as resolved.
Show resolved Hide resolved
*
* @param key - the backup decryption key
* @param version - the backup version corresponding to this decryption key
*
richvdh marked this conversation as resolved.
Show resolved Hide resolved
*/
storeSessionBackupPrivateKey(key: Uint8Array): Promise<void>;
storeSessionBackupPrivateKey(key: Uint8Array, version: string): Promise<void>;
richvdh marked this conversation as resolved.
Show resolved Hide resolved

/**
* Get the current status of key backup.
Expand Down
2 changes: 1 addition & 1 deletion src/crypto/CrossSigning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,7 @@ export async function requestKeysDuringVerification(
logger.info("Got key backup key, decoding...");
const decodedKey = decodeBase64(base64Key);
logger.info("Decoded backup key, storing...");
await client.crypto!.storeSessionBackupPrivateKey(Uint8Array.from(decodedKey));
await client.crypto!.storeSessionBackupPrivateKey(Uint8Array.from(decodedKey), "1");
richvdh marked this conversation as resolved.
Show resolved Hide resolved
logger.info("Backup key stored. Starting backup restore...");
const backupInfo = await client.getKeyBackupVersion();
// no need to await for this - just let it go in the bg
Expand Down
4 changes: 2 additions & 2 deletions src/crypto/EncryptionSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ export class EncryptionSetupBuilder {
});
}
// store session backup key in cache
if (this.sessionBackupPrivateKey) {
await crypto.storeSessionBackupPrivateKey(this.sessionBackupPrivateKey);
if (this.sessionBackupPrivateKey && this.keyBackupInfo?.version) {
await crypto.storeSessionBackupPrivateKey(this.sessionBackupPrivateKey, this.keyBackupInfo.version);
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1179,7 +1179,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// write the key to 4S
const privateKey = info.privateKey;
await this.secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey));
await this.storeSessionBackupPrivateKey(privateKey);
await this.storeSessionBackupPrivateKey(privateKey, version);

await this.backupManager.checkAndStart();
}
Expand Down Expand Up @@ -1302,7 +1302,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// make sure we have a Uint8Array, rather than a string
if (typeof encodedKey === "string") {
key = new Uint8Array(olmlib.decodeBase64(fixBackupKey(encodedKey) || encodedKey));
await this.storeSessionBackupPrivateKey(key);
// legacy crypto doesn't care about the version
await this.storeSessionBackupPrivateKey(key, "");
richvdh marked this conversation as resolved.
Show resolved Hide resolved
}
if (encodedKey && typeof encodedKey === "object" && "ciphertext" in encodedKey) {
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
Expand All @@ -1317,7 +1318,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @param key - the private key
* @returns a promise so you can catch failures
*/
public async storeSessionBackupPrivateKey(key: ArrayLike<number>): Promise<void> {
public async storeSessionBackupPrivateKey(key: ArrayLike<number>, version: string): Promise<void> {
if (!(key instanceof Uint8Array)) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`);
Expand Down
9 changes: 6 additions & 3 deletions src/rust-crypto/rust-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1005,12 +1005,15 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
* Implementation of {@link CryptoApi#storeSessionBackupPrivateKey}.
*
* @param key - the backup decryption key
* @param version - the backup version for this key. If not provided will fetch the current version from server.
richvdh marked this conversation as resolved.
Show resolved Hide resolved
*/
public async storeSessionBackupPrivateKey(key: Uint8Array): Promise<void> {
public async storeSessionBackupPrivateKey(key: Uint8Array, version: string): Promise<void> {
const base64Key = encodeBase64(key);

// TODO get version from backupManager
await this.olmMachine.saveBackupDecryptionKey(RustSdkCryptoJs.BackupDecryptionKey.fromBase64(base64Key), "");
await this.olmMachine.saveBackupDecryptionKey(
RustSdkCryptoJs.BackupDecryptionKey.fromBase64(base64Key),
version,
);
}

/**
Expand Down