You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-28 05:03:59 +03:00
factor out backup management to a separate module
This commit is contained in:
115
src/client.js
115
src/client.js
@@ -2,7 +2,7 @@
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018-2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -57,6 +57,7 @@ import {encodeBase64, decodeBase64} from "./crypto/olmlib";
|
||||
import { User } from "./models/user";
|
||||
import {AutoDiscovery} from "./autodiscovery";
|
||||
import {DEHYDRATION_ALGORITHM} from "./crypto/dehydration";
|
||||
import { BackupManager } from "./crypto/backup";
|
||||
|
||||
const SCROLLBACK_DELAY_MS = 3000;
|
||||
export const CRYPTO_ENABLED = isCryptoAvailable();
|
||||
@@ -258,6 +259,10 @@ function keyFromRecoverySession(session, decryptionKey) {
|
||||
* }
|
||||
* {string} name the name of the value we want to read out of SSSS, for UI purposes.
|
||||
*
|
||||
* @param {function} [opts.cryptoCallbacks.getBackupKey]
|
||||
* Optional. Function called when the backup key is required. The callback function
|
||||
* should return a promise that resolves to the backup key as a Uint8Array.
|
||||
*
|
||||
* @param {function} [opts.cryptoCallbacks.cacheSecretStorageKey]
|
||||
* Optional. Function called when a new encryption key for secret storage
|
||||
* has been created. This allows the application a chance to cache this key if
|
||||
@@ -1088,7 +1093,7 @@ MatrixClient.prototype.setDeviceVerified = function(userId, deviceId, verified)
|
||||
// check the key backup status, since whether or not we use this depends on
|
||||
// whether it has a signature from a verified device
|
||||
if (userId == this.credentials.userId) {
|
||||
this._crypto.checkKeyBackup();
|
||||
this.checkKeyBackup();
|
||||
}
|
||||
return prom;
|
||||
};
|
||||
@@ -1776,7 +1781,7 @@ MatrixClient.prototype.importRoomKeys = function(keys, opts) {
|
||||
* in trustInfo.
|
||||
*/
|
||||
MatrixClient.prototype.checkKeyBackup = function() {
|
||||
return this._crypto.checkKeyBackup();
|
||||
return this._crypto._backupManager.checkKeyBackup();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1818,7 +1823,7 @@ MatrixClient.prototype.getKeyBackupVersion = function() {
|
||||
* }
|
||||
*/
|
||||
MatrixClient.prototype.isKeyBackupTrusted = function(info) {
|
||||
return this._crypto.isKeyBackupTrusted(info);
|
||||
return this._crypto._backupManager.isKeyBackupTrusted(info);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1830,10 +1835,7 @@ MatrixClient.prototype.getKeyBackupEnabled = function() {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
if (!this._crypto._checkedForBackup) {
|
||||
return null;
|
||||
}
|
||||
return Boolean(this._crypto.backupKey);
|
||||
return this._crypto._backupManager.getKeyBackupEnabled();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1847,16 +1849,7 @@ MatrixClient.prototype.enableKeyBackup = function(info) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
this._crypto.backupInfo = info;
|
||||
if (this._crypto.backupKey) this._crypto.backupKey.free();
|
||||
this._crypto.backupKey = new global.Olm.PkEncryption();
|
||||
this._crypto.backupKey.set_recipient_key(info.auth_data.public_key);
|
||||
|
||||
this.emit('crypto.keyBackupStatus', true);
|
||||
|
||||
// There may be keys left over from a partially completed backup, so
|
||||
// schedule a send to check.
|
||||
this._crypto.scheduleKeyBackupSend();
|
||||
return this._crypto.backupManager.enableKeyBackup();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1867,11 +1860,7 @@ MatrixClient.prototype.disableKeyBackup = function() {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
this._crypto.backupInfo = null;
|
||||
if (this._crypto.backupKey) this._crypto.backupKey.free();
|
||||
this._crypto.backupKey = null;
|
||||
|
||||
this.emit('crypto.keyBackupStatus', false);
|
||||
this._crypto.backupManager.disableKeyBackup();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1896,26 +1885,19 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
const { keyInfo, encodedPrivateKey, privateKey } =
|
||||
await this.createRecoveryKeyFromPassphrase(password);
|
||||
// eslint-disable-next-line camelcase
|
||||
const {algorithm, auth_data, recovery_key, privateKey} =
|
||||
await this._crypto._backupManager.prepareKeyBackupVersion(password);
|
||||
|
||||
if (secureSecretStorage) {
|
||||
await this.storeSecret("m.megolm_backup.v1", encodeBase64(privateKey));
|
||||
logger.info("Key backup private key stored in secret storage");
|
||||
}
|
||||
|
||||
// Reshape objects into form expected for key backup
|
||||
const authData = {
|
||||
public_key: keyInfo.pubkey,
|
||||
};
|
||||
if (keyInfo.passphrase) {
|
||||
authData.private_key_salt = keyInfo.passphrase.salt;
|
||||
authData.private_key_iterations = keyInfo.passphrase.iterations;
|
||||
}
|
||||
return {
|
||||
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
|
||||
auth_data: authData,
|
||||
recovery_key: encodedPrivateKey,
|
||||
algorithm,
|
||||
auth_data,
|
||||
recovery_key,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1941,6 +1923,8 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
await this._crypto._backupManager.createKeyBackupVersion(info);
|
||||
|
||||
const data = {
|
||||
algorithm: info.algorithm,
|
||||
auth_data: info.auth_data,
|
||||
@@ -1986,8 +1970,8 @@ MatrixClient.prototype.deleteKeyBackupVersion = function(version) {
|
||||
// If we're currently backing up to this backup... stop.
|
||||
// (We start using it automatically in createKeyBackupVersion
|
||||
// so this is symmetrical).
|
||||
if (this._crypto.backupInfo && this._crypto.backupInfo.version === version) {
|
||||
this.disableKeyBackup();
|
||||
if (this._crypto._backupManager.version) {
|
||||
this._crypto._backupManager.disableKeyBackup();
|
||||
}
|
||||
|
||||
const path = utils.encodeUri("/room_keys/version/$version", {
|
||||
@@ -2051,7 +2035,7 @@ MatrixClient.prototype.scheduleAllGroupSessionsForBackup = async function() {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
await this._crypto.scheduleAllGroupSessionsForBackup();
|
||||
await this._crypto._backupManager.scheduleAllGroupSessionsForBackup();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2064,7 +2048,7 @@ MatrixClient.prototype.flagAllGroupSessionsForBackup = function() {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
|
||||
return this._crypto.flagAllGroupSessionsForBackup();
|
||||
return this._crypto._backupManager.flagAllGroupSessionsForBackup();
|
||||
};
|
||||
|
||||
MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
|
||||
@@ -2208,7 +2192,7 @@ MatrixClient.prototype.restoreKeyBackupWithCache = async function(
|
||||
);
|
||||
};
|
||||
|
||||
MatrixClient.prototype._restoreKeyBackup = function(
|
||||
MatrixClient.prototype._restoreKeyBackup = async function(
|
||||
privKey, targetRoomId, targetSessionId, backupInfo,
|
||||
{
|
||||
cacheCompleteCallback, // For sequencing during tests
|
||||
@@ -2225,19 +2209,13 @@ MatrixClient.prototype._restoreKeyBackup = function(
|
||||
targetRoomId, targetSessionId, backupInfo.version,
|
||||
);
|
||||
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
let backupPubKey;
|
||||
try {
|
||||
backupPubKey = decryption.init_with_private_key(privKey);
|
||||
} catch (e) {
|
||||
decryption.free();
|
||||
throw e;
|
||||
}
|
||||
const algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => { return privKey; });
|
||||
|
||||
try {
|
||||
// If the pubkey computed from the private data we've been given
|
||||
// doesn't match the one in the auth_data, the user has enetered
|
||||
// doesn't match the one in the auth_data, the user has entered
|
||||
// a different recovery key / the wrong passphrase.
|
||||
if (backupPubKey !== backupInfo.auth_data.public_key) {
|
||||
if (!await algorithm.keyMatches(privKey)) {
|
||||
return Promise.reject({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY });
|
||||
}
|
||||
|
||||
@@ -2254,17 +2232,18 @@ MatrixClient.prototype._restoreKeyBackup = function(
|
||||
});
|
||||
}
|
||||
|
||||
return this._http.authedRequest(
|
||||
const res = await this._http.authedRequest(
|
||||
undefined, "GET", path.path, path.queryData, undefined,
|
||||
{ prefix: PREFIX_UNSTABLE },
|
||||
).then((res) => {
|
||||
);
|
||||
|
||||
if (res.rooms) {
|
||||
for (const [roomId, roomData] of Object.entries(res.rooms)) {
|
||||
if (!roomData.sessions) continue;
|
||||
|
||||
totalKeyCount += Object.keys(roomData.sessions).length;
|
||||
const roomKeys = keysFromRecoverySession(
|
||||
roomData.sessions, decryption, roomId,
|
||||
const roomKeys = await algorithm.decryptSessions(
|
||||
roomData.sessions,
|
||||
);
|
||||
for (const k of roomKeys) {
|
||||
k.room_id = roomId;
|
||||
@@ -2273,13 +2252,18 @@ MatrixClient.prototype._restoreKeyBackup = function(
|
||||
}
|
||||
} else if (res.sessions) {
|
||||
totalKeyCount = Object.keys(res.sessions).length;
|
||||
keys = keysFromRecoverySession(
|
||||
res.sessions, decryption, targetRoomId, keys,
|
||||
keys = await algorithm.decryptSessions(
|
||||
res.sessions,
|
||||
);
|
||||
for (const k of keys) {
|
||||
k.room_id = targetRoomId;
|
||||
}
|
||||
} else {
|
||||
totalKeyCount = 1;
|
||||
try {
|
||||
const key = keyFromRecoverySession(res, decryption);
|
||||
const [key] = await algorithm.decryptSessions({
|
||||
[targetSessionId]: res,
|
||||
});
|
||||
key.room_id = targetRoomId;
|
||||
key.session_id = targetSessionId;
|
||||
keys.push(key);
|
||||
@@ -2287,19 +2271,20 @@ MatrixClient.prototype._restoreKeyBackup = function(
|
||||
logger.log("Failed to decrypt megolm session from backup", e);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
algorithm.free();
|
||||
}
|
||||
|
||||
return this.importRoomKeys(keys, {
|
||||
await this.importRoomKeys(keys, {
|
||||
progressCallback,
|
||||
untrusted: true,
|
||||
source: "backup",
|
||||
});
|
||||
}).then(() => {
|
||||
return this._crypto.setTrustedBackupPubKey(backupPubKey);
|
||||
}).then(() => {
|
||||
|
||||
// await this._crypto.setTrustedBackupPubKey(backupPubKey);
|
||||
await this.checkKeyBackup();
|
||||
|
||||
return {total: totalKeyCount, imported: keys.length};
|
||||
}).finally(() => {
|
||||
decryption.free();
|
||||
});
|
||||
};
|
||||
|
||||
MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, version) {
|
||||
|
||||
@@ -415,9 +415,8 @@ MegolmEncryption.prototype._prepareNewSession = async function(sharedHistory) {
|
||||
);
|
||||
|
||||
// don't wait for it to complete
|
||||
this._crypto.backupGroupSession(
|
||||
this._roomId, this._olmDevice.deviceCurve25519Key, [],
|
||||
sessionId, key.key,
|
||||
this._crypto._backupManager.backupGroupSession(
|
||||
this._olmDevice.deviceCurve25519Key, sessionId,
|
||||
);
|
||||
|
||||
return new OutboundSessionInfo(sessionId, sharedHistory);
|
||||
@@ -1428,11 +1427,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
});
|
||||
}).then(() => {
|
||||
// don't wait for the keys to be backed up for the server
|
||||
this._crypto.backupGroupSession(
|
||||
content.room_id, senderKey, forwardingKeyChain,
|
||||
content.session_id, content.session_key, keysClaimed,
|
||||
exportFormat,
|
||||
);
|
||||
this._crypto._backupManager.backupGroupSession(senderKey, content.session_id);
|
||||
}).catch((e) => {
|
||||
logger.error(`Error handling m.room_key_event: ${e}`);
|
||||
});
|
||||
@@ -1648,14 +1643,8 @@ MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
|
||||
).then(() => {
|
||||
if (opts.source !== "backup") {
|
||||
// don't wait for it to complete
|
||||
this._crypto.backupGroupSession(
|
||||
session.room_id,
|
||||
session.sender_key,
|
||||
session.forwarding_curve25519_key_chain,
|
||||
session.session_id,
|
||||
session.session_key,
|
||||
session.sender_claimed_keys,
|
||||
true,
|
||||
this._crypto._backupManager.backupGroupSession(
|
||||
session.sender_key, session.session_id,
|
||||
).catch((e) => {
|
||||
// This throws if the upload failed, but this is fine
|
||||
// since it will have written it to the db and will retry.
|
||||
|
||||
649
src/crypto/backup.ts
Normal file
649
src/crypto/backup.ts
Normal file
@@ -0,0 +1,649 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module crypto/backup
|
||||
*
|
||||
* Classes for dealing with key backup.
|
||||
*/
|
||||
|
||||
import {MatrixClient} from "../client";
|
||||
import {logger} from "../logger";
|
||||
import {MEGOLM_ALGORITHM, verifySignature} from "./olmlib";
|
||||
import {DeviceInfo} from "./deviceinfo"
|
||||
import {DeviceTrustLevel} from './CrossSigning';
|
||||
import {keyFromPassphrase} from './key_passphrase';
|
||||
import {sleep} from "../utils";
|
||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||
import {encodeRecoveryKey} from './recoverykey';
|
||||
|
||||
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||
|
||||
type AuthData = Record<string, any>;
|
||||
|
||||
type BackupInfo = {
|
||||
algorithm: string,
|
||||
auth_data: AuthData, // eslint-disable-line camelcase
|
||||
[properties: string]: any,
|
||||
};
|
||||
|
||||
type SigInfo = {
|
||||
deviceId: string,
|
||||
valid?: boolean | null, // true: valid, false: invalid, null: cannot attempt validation
|
||||
device?: DeviceInfo | null,
|
||||
crossSigningId?: boolean,
|
||||
deviceTrust?: DeviceTrustLevel,
|
||||
};
|
||||
|
||||
type TrustInfo = {
|
||||
usable: boolean, // is the backup trusted, true iff there is a sig that is valid & from a trusted device
|
||||
sigs: SigInfo[],
|
||||
};
|
||||
|
||||
/** A function used to get the secret key for a backup.
|
||||
*/
|
||||
type GetKey = () => Promise<Uint8Array>;
|
||||
|
||||
interface BackupAlgorithmClass {
|
||||
algorithmName: string;
|
||||
// initialize from an existing backup
|
||||
init(authData: AuthData, getKey: GetKey): Promise<BackupAlgorithm>;
|
||||
|
||||
// prepare a brand new backup
|
||||
prepare(
|
||||
key: string | Uint8Array | null,
|
||||
): Promise<[Uint8Array, AuthData]>;
|
||||
}
|
||||
|
||||
interface BackupAlgorithm {
|
||||
encryptSession(data: Record<string, any>): Promise<any>;
|
||||
decryptSessions(ciphertexts: Record<string, any>): Promise<Record<string, any>[]>;
|
||||
authData: AuthData;
|
||||
keyMatches(key: Uint8Array): Promise<boolean>;
|
||||
free(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the key backup.
|
||||
*/
|
||||
export class BackupManager {
|
||||
private algorithm: BackupAlgorithm | undefined;
|
||||
private backupInfo: BackupInfo | undefined; // The info dict from /room_keys/version
|
||||
public checkedForBackup: boolean; // Have we checked the server for a backup we can use?
|
||||
private sendingBackups: boolean; // Are we currently sending backups?
|
||||
constructor(private readonly baseApis, readonly getKey: GetKey) {
|
||||
this.checkedForBackup = false;
|
||||
this.sendingBackups = false;
|
||||
}
|
||||
|
||||
get version(): string | undefined {
|
||||
return this.backupInfo && this.backupInfo.version;
|
||||
}
|
||||
|
||||
static async makeAlgorithm(info: BackupInfo, getKey: GetKey): Promise<BackupAlgorithm> {
|
||||
const Algorithm = algorithmsByName[info.algorithm];
|
||||
if (!Algorithm) {
|
||||
throw new Error("Unknown backup algorithm");
|
||||
}
|
||||
return await Algorithm.init(info.auth_data, getKey);
|
||||
}
|
||||
|
||||
async enableKeyBackup(info: BackupInfo): Promise<void> {
|
||||
this.backupInfo = info;
|
||||
if (this.algorithm) {
|
||||
this.algorithm.free();
|
||||
}
|
||||
|
||||
this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
|
||||
|
||||
this.baseApis.emit('crypto.keyBackupStatus', true);
|
||||
|
||||
// There may be keys left over from a partially completed backup, so
|
||||
// schedule a send to check.
|
||||
this.scheduleKeyBackupSend();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable backing up of keys.
|
||||
*/
|
||||
disableKeyBackup(): void {
|
||||
if (this.algorithm) {
|
||||
this.algorithm.free();
|
||||
}
|
||||
this.algorithm = undefined;
|
||||
|
||||
this.backupInfo = undefined;
|
||||
|
||||
this.baseApis.emit('crypto.keyBackupStatus', false);
|
||||
}
|
||||
|
||||
getKeyBackupEnabled(): boolean | null {
|
||||
if (!this.checkedForBackup) {
|
||||
return null;
|
||||
}
|
||||
return Boolean(this.algorithm);
|
||||
}
|
||||
|
||||
async prepareKeyBackupVersion(
|
||||
key?: string | Uint8Array | null,
|
||||
algorithm?: string | undefined,
|
||||
): Promise<BackupInfo> {
|
||||
const Algorithm = algorithm ? algorithmsByName[algorithm] : DefaultAlgorithm;
|
||||
if (!Algorithm) {
|
||||
throw new Error("Unknown backup algorithm");
|
||||
}
|
||||
|
||||
const [privateKey, authData] = await Algorithm.prepare(key);
|
||||
const recoveryKey = encodeRecoveryKey(privateKey);
|
||||
return {
|
||||
algorithm: Algorithm.algorithmName,
|
||||
auth_data: authData,
|
||||
recovery_key: recoveryKey,
|
||||
privateKey,
|
||||
};
|
||||
}
|
||||
|
||||
async createKeyBackupVersion(info: BackupInfo): Promise<void> {
|
||||
this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the server for an active key backup and
|
||||
* if one is present and has a valid signature from
|
||||
* one of the user's verified devices, start backing up
|
||||
* to it.
|
||||
*/
|
||||
async checkAndStart(): Promise<{backupInfo: any, trustInfo: TrustInfo}> {
|
||||
logger.log("Checking key backup status...");
|
||||
if (this.baseApis.isGuest()) {
|
||||
logger.log("Skipping key backup check since user is guest");
|
||||
this.checkedForBackup = true;
|
||||
return null;
|
||||
}
|
||||
let backupInfo: BackupInfo;
|
||||
try {
|
||||
backupInfo = await this.baseApis.getKeyBackupVersion();
|
||||
} catch (e) {
|
||||
logger.log("Error checking for active key backup", e);
|
||||
if (e.httpStatus === 404) {
|
||||
// 404 is returned when the key backup does not exist, so that
|
||||
// counts as successfully checking.
|
||||
this.checkedForBackup = true;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
this.checkedForBackup = true;
|
||||
|
||||
const trustInfo = await this.isKeyBackupTrusted(backupInfo);
|
||||
|
||||
if (trustInfo.usable && !this.backupInfo) {
|
||||
logger.log(
|
||||
"Found usable key backup v" + backupInfo.version +
|
||||
": enabling key backups",
|
||||
);
|
||||
await this.enableKeyBackup(backupInfo);
|
||||
} else if (!trustInfo.usable && this.backupInfo) {
|
||||
logger.log("No usable key backup: disabling key backup");
|
||||
this.disableKeyBackup();
|
||||
} else if (!trustInfo.usable && !this.backupInfo) {
|
||||
logger.log("No usable key backup: not enabling key backup");
|
||||
} else if (trustInfo.usable && this.backupInfo) {
|
||||
// may not be the same version: if not, we should switch
|
||||
if (backupInfo.version !== this.backupInfo.version) {
|
||||
logger.log(
|
||||
"On backup version " + this.backupInfo.version + " but found " +
|
||||
"version " + backupInfo.version + ": switching.",
|
||||
);
|
||||
this.disableKeyBackup();
|
||||
await this.enableKeyBackup(backupInfo);
|
||||
// We're now using a new backup, so schedule all the keys we have to be
|
||||
// uploaded to the new backup. This is a bit of a workaround to upload
|
||||
// keys to a new backup in *most* cases, but it won't cover all cases
|
||||
// because we don't remember what backup version we uploaded keys to:
|
||||
// see https://github.com/vector-im/element-web/issues/14833
|
||||
await this.scheduleAllGroupSessionsForBackup();
|
||||
} else {
|
||||
logger.log("Backup version " + backupInfo.version + " still current");
|
||||
}
|
||||
}
|
||||
|
||||
return {backupInfo, trustInfo};
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces a re-check of the key backup and enables/disables it
|
||||
* as appropriate.
|
||||
*
|
||||
* @return {Object} Object with backup info (as returned by
|
||||
* getKeyBackupVersion) in backupInfo and
|
||||
* trust information (as returned by isKeyBackupTrusted)
|
||||
* in trustInfo.
|
||||
*/
|
||||
async checkKeyBackup(): Promise<{backupInfo: AuthData, trustInfo: TrustInfo}> {
|
||||
this.checkedForBackup = false;
|
||||
return this.checkAndStart();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} backupInfo key backup info dict from /room_keys/version
|
||||
* @return {object} {
|
||||
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
|
||||
* sigs: [
|
||||
* valid: [bool || null], // true: valid, false: invalid, null: cannot attempt validation
|
||||
* deviceId: [string],
|
||||
* device: [DeviceInfo || null],
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
async isKeyBackupTrusted(backupInfo: any | undefined): Promise<TrustInfo> {
|
||||
const ret = {
|
||||
usable: false,
|
||||
trusted_locally: false,
|
||||
sigs: [],
|
||||
};
|
||||
|
||||
if (
|
||||
!backupInfo ||
|
||||
!backupInfo.algorithm ||
|
||||
!backupInfo.auth_data ||
|
||||
!backupInfo.auth_data.public_key ||
|
||||
!backupInfo.auth_data.signatures
|
||||
) {
|
||||
logger.info("Key backup is absent or missing required data");
|
||||
return ret;
|
||||
}
|
||||
|
||||
const trustedPubkey = this.baseApis._crypto._sessionStore.getLocalTrustedBackupPubKey();
|
||||
|
||||
if (backupInfo.auth_data.public_key === trustedPubkey) {
|
||||
logger.info("Backup public key " + trustedPubkey + " is trusted locally");
|
||||
ret.trusted_locally = true;
|
||||
}
|
||||
|
||||
const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || [];
|
||||
|
||||
for (const keyId of Object.keys(mySigs)) {
|
||||
const keyIdParts = keyId.split(':');
|
||||
if (keyIdParts[0] !== 'ed25519') {
|
||||
logger.log("Ignoring unknown signature type: " + keyIdParts[0]);
|
||||
continue;
|
||||
}
|
||||
// Could be a cross-signing master key, but just say this is the device
|
||||
// ID for backwards compat
|
||||
const sigInfo: SigInfo = { deviceId: keyIdParts[1] };
|
||||
|
||||
// first check to see if it's from our cross-signing key
|
||||
const crossSigningId = this.baseApis._crypto._crossSigningInfo.getId();
|
||||
if (crossSigningId === sigInfo.deviceId) {
|
||||
sigInfo.crossSigningId = true;
|
||||
try {
|
||||
await verifySignature(
|
||||
this.baseApis._crypto._olmDevice,
|
||||
backupInfo.auth_data,
|
||||
this.baseApis.getUserId(),
|
||||
sigInfo.deviceId,
|
||||
crossSigningId,
|
||||
);
|
||||
sigInfo.valid = true;
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
"Bad signature from cross signing key " + crossSigningId, e,
|
||||
);
|
||||
sigInfo.valid = false;
|
||||
}
|
||||
ret.sigs.push(sigInfo);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Now look for a sig from a device
|
||||
// At some point this can probably go away and we'll just support
|
||||
// it being signed by the cross-signing master key
|
||||
const device = this.baseApis._crypto._deviceList.getStoredDevice(
|
||||
this.baseApis.getUserId(), sigInfo.deviceId,
|
||||
);
|
||||
if (device) {
|
||||
sigInfo.device = device;
|
||||
sigInfo.deviceTrust = await this.baseApis.checkDeviceTrust(
|
||||
this.baseApis.getUserId(), sigInfo.deviceId,
|
||||
);
|
||||
try {
|
||||
await verifySignature(
|
||||
this.baseApis._crypto._olmDevice,
|
||||
backupInfo.auth_data,
|
||||
this.baseApis.getUserId(),
|
||||
device.deviceId,
|
||||
device.getFingerprint(),
|
||||
);
|
||||
sigInfo.valid = true;
|
||||
} catch (e) {
|
||||
logger.info(
|
||||
"Bad signature from key ID " + keyId + " userID " + this.baseApis.getUserId() +
|
||||
" device ID " + device.deviceId + " fingerprint: " +
|
||||
device.getFingerprint(), backupInfo.auth_data, e,
|
||||
);
|
||||
sigInfo.valid = false;
|
||||
}
|
||||
} else {
|
||||
sigInfo.valid = null; // Can't determine validity because we don't have the signing device
|
||||
logger.info("Ignoring signature from unknown key " + keyId);
|
||||
}
|
||||
ret.sigs.push(sigInfo);
|
||||
}
|
||||
|
||||
ret.usable = ret.sigs.some((s) => {
|
||||
return (
|
||||
s.valid && (
|
||||
(s.device && s.deviceTrust.isVerified()) ||
|
||||
(s.crossSigningId)
|
||||
)
|
||||
);
|
||||
});
|
||||
ret.usable ||= ret.trusted_locally;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules sending all keys waiting to be sent to the backup, if not already
|
||||
* scheduled. Retries if necessary.
|
||||
*
|
||||
* @param maxDelay Maximum delay to wait in ms. 0 means no delay.
|
||||
*/
|
||||
async scheduleKeyBackupSend(maxDelay = 10000): Promise<void> {
|
||||
if (this.sendingBackups) return;
|
||||
|
||||
this.sendingBackups = true;
|
||||
|
||||
try {
|
||||
// wait between 0 and `maxDelay` seconds, to avoid backup
|
||||
// requests from different clients hitting the server all at
|
||||
// the same time when a new key is sent
|
||||
const delay = Math.random() * maxDelay;
|
||||
await sleep(delay, undefined);
|
||||
let numFailures = 0; // number of consecutive failures
|
||||
for (;;) {
|
||||
if (!this.algorithm) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const numBackedUp =
|
||||
await this.backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST);
|
||||
if (numBackedUp === 0) {
|
||||
// no sessions left needing backup: we're done
|
||||
return;
|
||||
}
|
||||
numFailures = 0;
|
||||
} catch (err) {
|
||||
numFailures++;
|
||||
logger.log("Key backup request failed", err);
|
||||
if (err.data) {
|
||||
if (
|
||||
err.data.errcode == 'M_NOT_FOUND' ||
|
||||
err.data.errcode == 'M_WRONG_ROOM_KEYS_VERSION'
|
||||
) {
|
||||
// Re-check key backup status on error, so we can be
|
||||
// sure to present the current situation when asked.
|
||||
await this.checkKeyBackup();
|
||||
// Backup version has changed or this backup version
|
||||
// has been deleted
|
||||
this.baseApis._crypto.emit("crypto.keyBackupFailed", err.data.errcode);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (numFailures) {
|
||||
// exponential backoff if we have failures
|
||||
await sleep(1000 * Math.pow(2, Math.min(numFailures - 1, 4)), undefined);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.sendingBackups = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Take some e2e keys waiting to be backed up and send them
|
||||
* to the backup.
|
||||
*
|
||||
* @param {integer} limit Maximum number of keys to back up
|
||||
* @returns {integer} Number of sessions backed up
|
||||
*/
|
||||
private async backupPendingKeys(limit: number): Promise<number> {
|
||||
const sessions = await this.baseApis._crypto._cryptoStore.getSessionsNeedingBackup(limit);
|
||||
if (!sessions.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let remaining = await this.baseApis._crypto._cryptoStore.countSessionsNeedingBackup();
|
||||
this.baseApis._crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
|
||||
const data = {};
|
||||
for (const session of sessions) {
|
||||
const roomId = session.sessionData.room_id;
|
||||
if (data[roomId] === undefined) {
|
||||
data[roomId] = {sessions: {}};
|
||||
}
|
||||
|
||||
const sessionData = await this.baseApis._crypto._olmDevice.exportInboundGroupSession(
|
||||
session.senderKey, session.sessionId, session.sessionData,
|
||||
);
|
||||
sessionData.algorithm = MEGOLM_ALGORITHM;
|
||||
|
||||
const forwardedCount =
|
||||
(sessionData.forwarding_curve25519_key_chain || []).length;
|
||||
|
||||
const userId = this.baseApis._crypto._deviceList.getUserByIdentityKey(
|
||||
MEGOLM_ALGORITHM, session.senderKey,
|
||||
);
|
||||
const device = this.baseApis._crypto._deviceList.getDeviceByIdentityKey(
|
||||
MEGOLM_ALGORITHM, session.senderKey,
|
||||
);
|
||||
const verified = this.baseApis._crypto._checkDeviceInfoTrust(userId, device).isVerified();
|
||||
|
||||
data[roomId]['sessions'][session.sessionId] = {
|
||||
first_message_index: sessionData.first_known_index,
|
||||
forwarded_count: forwardedCount,
|
||||
is_verified: verified,
|
||||
session_data: await this.algorithm.encryptSession(sessionData),
|
||||
};
|
||||
}
|
||||
|
||||
await this.baseApis.sendKeyBackup(
|
||||
undefined, undefined, this.backupInfo.version,
|
||||
{rooms: data},
|
||||
);
|
||||
|
||||
await this.baseApis._crypto._cryptoStore.unmarkSessionsNeedingBackup(sessions);
|
||||
remaining = await this.baseApis._crypto._cryptoStore.countSessionsNeedingBackup();
|
||||
this.baseApis._crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
|
||||
return sessions.length;
|
||||
}
|
||||
|
||||
async backupGroupSession(
|
||||
senderKey: string, sessionId: string,
|
||||
): Promise<void> {
|
||||
await this.baseApis._crypto._cryptoStore.markSessionsNeedingBackup([{
|
||||
senderKey: senderKey,
|
||||
sessionId: sessionId,
|
||||
}]);
|
||||
|
||||
if (this.backupInfo) {
|
||||
// don't wait for this to complete: it will delay so
|
||||
// happens in the background
|
||||
this.scheduleKeyBackupSend();
|
||||
}
|
||||
// if this.backupInfo is not set, then the keys will be backed up when
|
||||
// this.enableKeyBackup is called
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks all group sessions as needing to be backed up and schedules them to
|
||||
* upload in the background as soon as possible.
|
||||
*/
|
||||
async scheduleAllGroupSessionsForBackup(): Promise<void> {
|
||||
await this.flagAllGroupSessionsForBackup();
|
||||
|
||||
// Schedule keys to upload in the background as soon as possible.
|
||||
this.scheduleKeyBackupSend(0 /* maxDelay */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks all group sessions as needing to be backed up without scheduling
|
||||
* them to upload in the background.
|
||||
* @returns {Promise<int>} Resolves to the number of sessions now requiring a backup
|
||||
* (which will be equal to the number of sessions in the store).
|
||||
*/
|
||||
async flagAllGroupSessionsForBackup(): Promise<number> {
|
||||
await this.baseApis._crypto._cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
[
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||
IndexedDBCryptoStore.STORE_BACKUP,
|
||||
],
|
||||
(txn) => {
|
||||
this.baseApis._crypto._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => {
|
||||
if (session !== null) {
|
||||
this.baseApis._crypto._cryptoStore.markSessionsNeedingBackup([session], txn);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const remaining = await this.baseApis._crypto._cryptoStore.countSessionsNeedingBackup();
|
||||
this.baseApis.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
return remaining;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of end to end session keys that are waiting to be backed up
|
||||
* @returns {Promise<int>} Resolves to the number of sessions requiring backup
|
||||
*/
|
||||
countSessionsNeedingBackup(): Promise<number> {
|
||||
return this.baseApis._crypto._cryptoStore.countSessionsNeedingBackup();
|
||||
}
|
||||
}
|
||||
|
||||
export class Curve25519 implements BackupAlgorithm {
|
||||
static algorithmName = "m.megolm_backup.v1.curve25519-aes-sha2";
|
||||
|
||||
constructor(
|
||||
public authData: AuthData,
|
||||
private publicKey: any, // FIXME: PkEncryption
|
||||
private getKey: () => Promise<Uint8Array>,
|
||||
) {}
|
||||
|
||||
static async init(
|
||||
authData: AuthData,
|
||||
getKey: () => Promise<Uint8Array>,
|
||||
): Promise<Curve25519> {
|
||||
if (!authData || !authData.public_key) {
|
||||
throw new Error("auth_data missing required information");
|
||||
}
|
||||
const publicKey = new global.Olm.PkEncryption();
|
||||
publicKey.set_recipient_key(authData.public_key);
|
||||
return new Curve25519(authData, publicKey, getKey);
|
||||
}
|
||||
|
||||
static async prepare(
|
||||
key: string | Uint8Array | null,
|
||||
): Promise<[Uint8Array, AuthData]> {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
try {
|
||||
const authData: AuthData = {};
|
||||
if (!key) {
|
||||
authData.public_key = decryption.generate_key();
|
||||
} else if (key instanceof Uint8Array) {
|
||||
authData.public_key = decryption.init_with_private_key(key);
|
||||
} else {
|
||||
const derivation = await keyFromPassphrase(key);
|
||||
authData.private_key_salt = derivation.salt;
|
||||
authData.private_key_iterations = derivation.iterations;
|
||||
// FIXME: algorithm?
|
||||
authData.public_key = decryption.init_with_private_key(derivation.key);
|
||||
}
|
||||
const publicKey = new global.Olm.PkEncryption();
|
||||
publicKey.set_recipient_key(authData.public_key);
|
||||
|
||||
return [
|
||||
decryption.get_private_key(),
|
||||
authData,
|
||||
]
|
||||
} finally {
|
||||
decryption.free();
|
||||
}
|
||||
}
|
||||
|
||||
async encryptSession(data: Record<string, any>): Promise<any> {
|
||||
const plainText: Record<string, any> = Object.assign({}, data);
|
||||
delete plainText.session_id;
|
||||
delete plainText.room_id;
|
||||
delete plainText.first_known_index;
|
||||
return this.publicKey.encrypt(JSON.stringify(plainText));
|
||||
}
|
||||
|
||||
async decryptSessions(sessions: Record<string, Record<string, any>>): Promise<Record<string, any>[]> {
|
||||
const privKey = await this.getKey();
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
try {
|
||||
const backupPubKey = decryption.init_with_private_key(privKey);
|
||||
|
||||
if (backupPubKey !== this.authData.public_key) {
|
||||
throw {errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY};
|
||||
}
|
||||
|
||||
const keys = [];
|
||||
|
||||
for (const [sessionId, sessionData] of Object.entries(sessions)) {
|
||||
try {
|
||||
const decrypted = JSON.parse(decryption.decrypt(
|
||||
sessionData.session_data.ephemeral,
|
||||
sessionData.session_data.mac,
|
||||
sessionData.session_data.ciphertext,
|
||||
));
|
||||
decrypted.session_id = sessionId;
|
||||
keys.push(decrypted);
|
||||
} catch (e) {
|
||||
logger.log("Failed to decrypt megolm session from backup", e, sessionData);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
} finally {
|
||||
decryption.free();
|
||||
}
|
||||
}
|
||||
|
||||
async keyMatches(key: Uint8Array): Promise<boolean> {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
let pubKey;
|
||||
try {
|
||||
pubKey = decryption.init_with_private_key(key);
|
||||
} finally {
|
||||
decryption.free();
|
||||
}
|
||||
|
||||
return pubKey === this.authData.public_key;
|
||||
}
|
||||
|
||||
free(): void {
|
||||
this.publicKey.free();
|
||||
}
|
||||
}
|
||||
|
||||
export const algorithmsByName: Record<string, BackupAlgorithmClass> = {
|
||||
[Curve25519.algorithmName]: Curve25519,
|
||||
};
|
||||
|
||||
export const DefaultAlgorithm: BackupAlgorithmClass = Curve25519;
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018-2019 New Vector Ltd
|
||||
Copyright 2019-2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019-2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -58,6 +58,7 @@ import {KeySignatureUploadError} from "../errors";
|
||||
import {decryptAES, encryptAES} from './aes';
|
||||
import {DehydrationManager} from './dehydration';
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { BackupManager } from "./backup";
|
||||
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
@@ -85,7 +86,6 @@ export function isCryptoAvailable() {
|
||||
}
|
||||
|
||||
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
|
||||
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||
|
||||
/**
|
||||
* Cryptography bits
|
||||
@@ -154,13 +154,36 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
} else {
|
||||
this._verificationMethods = defaultVerificationMethods;
|
||||
}
|
||||
// track whether this device's megolm keys are being backed up incrementally
|
||||
// to the server or not.
|
||||
// XXX: this should probably have a single source of truth from OlmAccount
|
||||
this.backupInfo = null; // The info dict from /room_keys/version
|
||||
this.backupKey = null; // The encryption key object
|
||||
this._checkedForBackup = false; // Have we checked the server for a backup we can use?
|
||||
this._sendingBackups = false; // Are we currently sending backups?
|
||||
|
||||
this._backupManager = new BackupManager(baseApis, async (algorithm) => {
|
||||
// try to get key from cache
|
||||
const cachedKey = await this.getSessionBackupPrivateKey();
|
||||
if (cachedKey) {
|
||||
return cachedKey;
|
||||
}
|
||||
|
||||
// try to get key from secret storage
|
||||
const storedKey = await this.getSecret("m.megolm_backup.v1");
|
||||
|
||||
if (storedKey) {
|
||||
// ensure that the key is in the right format. If not, fix the key and
|
||||
// store the fixed version
|
||||
const fixedKey = fixBackupKey(storedKey);
|
||||
if (fixedKey) {
|
||||
const [keyId] = await this._crypto.getSecretStorageKey();
|
||||
await this.storeSecret("m.megolm_backup.v1", fixedKey, [keyId]);
|
||||
}
|
||||
|
||||
return decodeBase64(fixedKey || storedKey);
|
||||
}
|
||||
|
||||
// try to get key from app
|
||||
if (this._baseApis._cryptoCallbacks && this._baseApis._cryptoCallbacks.getBackupKey) {
|
||||
return await this._baseApis._cryptoCallbacks.getBackupKey(algorithm);
|
||||
}
|
||||
|
||||
throw new Error("Unable to get private key");
|
||||
});
|
||||
|
||||
this._olmDevice = new OlmDevice(cryptoStore);
|
||||
this._deviceList = new DeviceList(
|
||||
@@ -331,7 +354,7 @@ Crypto.prototype.init = async function(opts) {
|
||||
this._deviceList.startTrackingDeviceList(this._userId);
|
||||
|
||||
logger.log("Crypto: checking for key backup...");
|
||||
this._checkAndStartKeyBackup();
|
||||
this._backupManager.checkAndStart();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -458,7 +481,7 @@ Crypto.prototype.isSecretStorageReady = async function() {
|
||||
this._secretStorage,
|
||||
);
|
||||
const sessionBackupInStorage = (
|
||||
!this._baseApis.getKeyBackupEnabled() ||
|
||||
!this._backupManager.getKeyBackupEnabled() ||
|
||||
this._baseApis.isKeyBackupKeyStored()
|
||||
);
|
||||
|
||||
@@ -522,9 +545,11 @@ Crypto.prototype.bootstrapCrossSigning = async function({
|
||||
builder.addKeySignature(this._userId, this._deviceId, deviceSignature);
|
||||
|
||||
// Sign message key backup with cross-signing master key
|
||||
if (this.backupInfo) {
|
||||
await crossSigningInfo.signObject(this.backupInfo.auth_data, "master");
|
||||
builder.addSessionBackup(this.backupInfo);
|
||||
if (this._backupManager.backupInfo) {
|
||||
await crossSigningInfo.signObject(
|
||||
this._backupManager.backupInfo.auth_data, "master",
|
||||
);
|
||||
builder.addSessionBackup(this._backupManager.backupInfo);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -766,6 +791,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
|
||||
keyBackupInfo.auth_data.private_key_salt &&
|
||||
keyBackupInfo.auth_data.private_key_iterations
|
||||
) {
|
||||
// FIXME: ???
|
||||
opts.passphrase = {
|
||||
algorithm: "m.pbkdf2",
|
||||
iterations: keyBackupInfo.auth_data.private_key_iterations,
|
||||
@@ -1477,7 +1503,7 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function({
|
||||
}
|
||||
|
||||
// Now we may be able to trust our key backup
|
||||
await this.checkKeyBackup();
|
||||
await this._backupManager.checkKeyBackup();
|
||||
// FIXME: if we previously trusted the backup, should we automatically sign
|
||||
// the backup with the new key (if not already signed)?
|
||||
};
|
||||
@@ -1539,206 +1565,11 @@ Crypto.prototype._checkDeviceVerifications = async function(userId) {
|
||||
logger.info(`Finished device verification upgrade for ${userId}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check the server for an active key backup and
|
||||
* if one is present and has a valid signature from
|
||||
* one of the user's verified devices, start backing up
|
||||
* to it.
|
||||
*/
|
||||
Crypto.prototype._checkAndStartKeyBackup = async function() {
|
||||
logger.log("Checking key backup status...");
|
||||
if (this._baseApis.isGuest()) {
|
||||
logger.log("Skipping key backup check since user is guest");
|
||||
this._checkedForBackup = true;
|
||||
return null;
|
||||
}
|
||||
let backupInfo;
|
||||
try {
|
||||
backupInfo = await this._baseApis.getKeyBackupVersion();
|
||||
} catch (e) {
|
||||
logger.log("Error checking for active key backup", e);
|
||||
if (e.httpStatus === 404) {
|
||||
// 404 is returned when the key backup does not exist, so that
|
||||
// counts as successfully checking.
|
||||
this._checkedForBackup = true;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
this._checkedForBackup = true;
|
||||
|
||||
const trustInfo = await this.isKeyBackupTrusted(backupInfo);
|
||||
|
||||
if (trustInfo.usable && !this.backupInfo) {
|
||||
logger.log(
|
||||
"Found usable key backup v" + backupInfo.version +
|
||||
": enabling key backups",
|
||||
);
|
||||
this._baseApis.enableKeyBackup(backupInfo);
|
||||
} else if (!trustInfo.usable && this.backupInfo) {
|
||||
logger.log("No usable key backup: disabling key backup");
|
||||
this._baseApis.disableKeyBackup();
|
||||
} else if (!trustInfo.usable && !this.backupInfo) {
|
||||
logger.log("No usable key backup: not enabling key backup");
|
||||
} else if (trustInfo.usable && this.backupInfo) {
|
||||
// may not be the same version: if not, we should switch
|
||||
if (backupInfo.version !== this.backupInfo.version) {
|
||||
logger.log(
|
||||
"On backup version " + this.backupInfo.version + " but found " +
|
||||
"version " + backupInfo.version + ": switching.",
|
||||
);
|
||||
this._baseApis.disableKeyBackup();
|
||||
this._baseApis.enableKeyBackup(backupInfo);
|
||||
// We're now using a new backup, so schedule all the keys we have to be
|
||||
// uploaded to the new backup. This is a bit of a workaround to upload
|
||||
// keys to a new backup in *most* cases, but it won't cover all cases
|
||||
// because we don't remember what backup version we uploaded keys to:
|
||||
// see https://github.com/vector-im/element-web/issues/14833
|
||||
await this.scheduleAllGroupSessionsForBackup();
|
||||
} else {
|
||||
logger.log("Backup version " + backupInfo.version + " still current");
|
||||
}
|
||||
}
|
||||
|
||||
return {backupInfo, trustInfo};
|
||||
};
|
||||
|
||||
Crypto.prototype.setTrustedBackupPubKey = async function(trustedPubKey) {
|
||||
// This should be redundant post cross-signing is a thing, so just
|
||||
// plonk it in localStorage for now.
|
||||
this._sessionStore.setLocalTrustedBackupPubKey(trustedPubKey);
|
||||
await this.checkKeyBackup();
|
||||
};
|
||||
|
||||
/**
|
||||
* Forces a re-check of the key backup and enables/disables it
|
||||
* as appropriate.
|
||||
*
|
||||
* @return {Object} Object with backup info (as returned by
|
||||
* getKeyBackupVersion) in backupInfo and
|
||||
* trust information (as returned by isKeyBackupTrusted)
|
||||
* in trustInfo.
|
||||
*/
|
||||
Crypto.prototype.checkKeyBackup = async function() {
|
||||
this._checkedForBackup = false;
|
||||
return this._checkAndStartKeyBackup();
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {object} backupInfo key backup info dict from /room_keys/version
|
||||
* @return {object} {
|
||||
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
|
||||
* sigs: [
|
||||
* valid: [bool || null], // true: valid, false: invalid, null: cannot attempt validation
|
||||
* deviceId: [string],
|
||||
* device: [DeviceInfo || null],
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
|
||||
const ret = {
|
||||
usable: false,
|
||||
trusted_locally: false,
|
||||
sigs: [],
|
||||
};
|
||||
|
||||
if (
|
||||
!backupInfo ||
|
||||
!backupInfo.algorithm ||
|
||||
!backupInfo.auth_data ||
|
||||
!backupInfo.auth_data.public_key ||
|
||||
!backupInfo.auth_data.signatures
|
||||
) {
|
||||
logger.info("Key backup is absent or missing required data");
|
||||
return ret;
|
||||
}
|
||||
|
||||
const trustedPubkey = this._sessionStore.getLocalTrustedBackupPubKey();
|
||||
|
||||
if (backupInfo.auth_data.public_key === trustedPubkey) {
|
||||
logger.info("Backup public key " + trustedPubkey + " is trusted locally");
|
||||
ret.trusted_locally = true;
|
||||
}
|
||||
|
||||
const mySigs = backupInfo.auth_data.signatures[this._userId] || [];
|
||||
|
||||
for (const keyId of Object.keys(mySigs)) {
|
||||
const keyIdParts = keyId.split(':');
|
||||
if (keyIdParts[0] !== 'ed25519') {
|
||||
logger.log("Ignoring unknown signature type: " + keyIdParts[0]);
|
||||
continue;
|
||||
}
|
||||
// Could be a cross-signing master key, but just say this is the device
|
||||
// ID for backwards compat
|
||||
const sigInfo = { deviceId: keyIdParts[1] };
|
||||
|
||||
// first check to see if it's from our cross-signing key
|
||||
const crossSigningId = this._crossSigningInfo.getId();
|
||||
if (crossSigningId === sigInfo.deviceId) {
|
||||
sigInfo.crossSigningId = true;
|
||||
try {
|
||||
await olmlib.verifySignature(
|
||||
this._olmDevice,
|
||||
backupInfo.auth_data,
|
||||
this._userId,
|
||||
sigInfo.deviceId,
|
||||
crossSigningId,
|
||||
);
|
||||
sigInfo.valid = true;
|
||||
} catch (e) {
|
||||
logger.warning(
|
||||
"Bad signature from cross signing key " + crossSigningId, e,
|
||||
);
|
||||
sigInfo.valid = false;
|
||||
}
|
||||
ret.sigs.push(sigInfo);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Now look for a sig from a device
|
||||
// At some point this can probably go away and we'll just support
|
||||
// it being signed by the cross-signing master key
|
||||
const device = this._deviceList.getStoredDevice(
|
||||
this._userId, sigInfo.deviceId,
|
||||
);
|
||||
if (device) {
|
||||
sigInfo.device = device;
|
||||
sigInfo.deviceTrust = await this.checkDeviceTrust(
|
||||
this._userId, sigInfo.deviceId,
|
||||
);
|
||||
try {
|
||||
await olmlib.verifySignature(
|
||||
this._olmDevice,
|
||||
backupInfo.auth_data,
|
||||
this._userId,
|
||||
device.deviceId,
|
||||
device.getFingerprint(),
|
||||
);
|
||||
sigInfo.valid = true;
|
||||
} catch (e) {
|
||||
logger.info(
|
||||
"Bad signature from key ID " + keyId + " userID " + this._userId +
|
||||
" device ID " + device.deviceId + " fingerprint: " +
|
||||
device.getFingerprint(), backupInfo.auth_data, e,
|
||||
);
|
||||
sigInfo.valid = false;
|
||||
}
|
||||
} else {
|
||||
sigInfo.valid = null; // Can't determine validity because we don't have the signing device
|
||||
logger.info("Ignoring signature from unknown key " + keyId);
|
||||
}
|
||||
ret.sigs.push(sigInfo);
|
||||
}
|
||||
|
||||
ret.usable = ret.sigs.some((s) => {
|
||||
return (
|
||||
s.valid && (
|
||||
(s.device && s.deviceTrust.isVerified()) ||
|
||||
(s.crossSigningId)
|
||||
)
|
||||
);
|
||||
});
|
||||
ret.usable |= ret.trusted_locally;
|
||||
return ret;
|
||||
await this._backupManager.checkKeyBackup();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2789,191 +2620,12 @@ Crypto.prototype.importRoomKeys = function(keys, opts = {}) {
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Schedules sending all keys waiting to be sent to the backup, if not already
|
||||
* scheduled. Retries if necessary.
|
||||
*
|
||||
* @param {number} maxDelay Maximum delay to wait in ms. 0 means no delay.
|
||||
*/
|
||||
Crypto.prototype.scheduleKeyBackupSend = async function(maxDelay = 10000) {
|
||||
if (this._sendingBackups) return;
|
||||
|
||||
this._sendingBackups = true;
|
||||
|
||||
try {
|
||||
// wait between 0 and `maxDelay` seconds, to avoid backup
|
||||
// requests from different clients hitting the server all at
|
||||
// the same time when a new key is sent
|
||||
const delay = Math.random() * maxDelay;
|
||||
await sleep(delay);
|
||||
let numFailures = 0; // number of consecutive failures
|
||||
while (1) {
|
||||
if (!this.backupKey) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const numBackedUp =
|
||||
await this._backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST);
|
||||
if (numBackedUp === 0) {
|
||||
// no sessions left needing backup: we're done
|
||||
return;
|
||||
}
|
||||
numFailures = 0;
|
||||
} catch (err) {
|
||||
numFailures++;
|
||||
logger.log("Key backup request failed", err);
|
||||
if (err.data) {
|
||||
if (
|
||||
err.data.errcode == 'M_NOT_FOUND' ||
|
||||
err.data.errcode == 'M_WRONG_ROOM_KEYS_VERSION'
|
||||
) {
|
||||
// Re-check key backup status on error, so we can be
|
||||
// sure to present the current situation when asked.
|
||||
await this.checkKeyBackup();
|
||||
// Backup version has changed or this backup version
|
||||
// has been deleted
|
||||
this.emit("crypto.keyBackupFailed", err.data.errcode);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (numFailures) {
|
||||
// exponential backoff if we have failures
|
||||
await sleep(1000 * Math.pow(2, Math.min(numFailures - 1, 4)));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this._sendingBackups = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Take some e2e keys waiting to be backed up and send them
|
||||
* to the backup.
|
||||
*
|
||||
* @param {integer} limit Maximum number of keys to back up
|
||||
* @returns {integer} Number of sessions backed up
|
||||
*/
|
||||
Crypto.prototype._backupPendingKeys = async function(limit) {
|
||||
const sessions = await this._cryptoStore.getSessionsNeedingBackup(limit);
|
||||
if (!sessions.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let remaining = await this._cryptoStore.countSessionsNeedingBackup();
|
||||
this.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
|
||||
const data = {};
|
||||
for (const session of sessions) {
|
||||
const roomId = session.sessionData.room_id;
|
||||
if (data[roomId] === undefined) {
|
||||
data[roomId] = {sessions: {}};
|
||||
}
|
||||
|
||||
const sessionData = await this._olmDevice.exportInboundGroupSession(
|
||||
session.senderKey, session.sessionId, session.sessionData,
|
||||
);
|
||||
sessionData.algorithm = olmlib.MEGOLM_ALGORITHM;
|
||||
delete sessionData.session_id;
|
||||
delete sessionData.room_id;
|
||||
const firstKnownIndex = sessionData.first_known_index;
|
||||
delete sessionData.first_known_index;
|
||||
const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData));
|
||||
|
||||
const forwardedCount =
|
||||
(sessionData.forwarding_curve25519_key_chain || []).length;
|
||||
|
||||
const userId = this._deviceList.getUserByIdentityKey(
|
||||
olmlib.MEGOLM_ALGORITHM, session.senderKey,
|
||||
);
|
||||
const device = this._deviceList.getDeviceByIdentityKey(
|
||||
olmlib.MEGOLM_ALGORITHM, session.senderKey,
|
||||
);
|
||||
const verified = this._checkDeviceInfoTrust(userId, device).isVerified();
|
||||
|
||||
data[roomId]['sessions'][session.sessionId] = {
|
||||
first_message_index: firstKnownIndex,
|
||||
forwarded_count: forwardedCount,
|
||||
is_verified: verified,
|
||||
session_data: encrypted,
|
||||
};
|
||||
}
|
||||
|
||||
await this._baseApis.sendKeyBackup(
|
||||
undefined, undefined, this.backupInfo.version,
|
||||
{rooms: data},
|
||||
);
|
||||
|
||||
await this._cryptoStore.unmarkSessionsNeedingBackup(sessions);
|
||||
remaining = await this._cryptoStore.countSessionsNeedingBackup();
|
||||
this.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
|
||||
return sessions.length;
|
||||
};
|
||||
|
||||
Crypto.prototype.backupGroupSession = async function(
|
||||
roomId, senderKey, forwardingCurve25519KeyChain,
|
||||
sessionId, sessionKey, keysClaimed,
|
||||
exportFormat,
|
||||
) {
|
||||
await this._cryptoStore.markSessionsNeedingBackup([{
|
||||
senderKey: senderKey,
|
||||
sessionId: sessionId,
|
||||
}]);
|
||||
|
||||
if (this.backupInfo) {
|
||||
// don't wait for this to complete: it will delay so
|
||||
// happens in the background
|
||||
this.scheduleKeyBackupSend();
|
||||
}
|
||||
// if this.backupInfo is not set, then the keys will be backed up when
|
||||
// client.enableKeyBackup is called
|
||||
};
|
||||
|
||||
/**
|
||||
* Marks all group sessions as needing to be backed up and schedules them to
|
||||
* upload in the background as soon as possible.
|
||||
*/
|
||||
Crypto.prototype.scheduleAllGroupSessionsForBackup = async function() {
|
||||
await this.flagAllGroupSessionsForBackup();
|
||||
|
||||
// Schedule keys to upload in the background as soon as possible.
|
||||
this.scheduleKeyBackupSend(0 /* maxDelay */);
|
||||
};
|
||||
|
||||
/**
|
||||
* Marks all group sessions as needing to be backed up without scheduling
|
||||
* them to upload in the background.
|
||||
* @returns {Promise<int>} Resolves to the number of sessions now requiring a backup
|
||||
* (which will be equal to the number of sessions in the store).
|
||||
*/
|
||||
Crypto.prototype.flagAllGroupSessionsForBackup = async function() {
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
[
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||
IndexedDBCryptoStore.STORE_BACKUP,
|
||||
],
|
||||
(txn) => {
|
||||
this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => {
|
||||
if (session !== null) {
|
||||
this._cryptoStore.markSessionsNeedingBackup([session], txn);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const remaining = await this._cryptoStore.countSessionsNeedingBackup();
|
||||
this.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
return remaining;
|
||||
};
|
||||
|
||||
/**
|
||||
* Counts the number of end to end session keys that are waiting to be backed up
|
||||
* @returns {Promise<int>} Resolves to the number of sessions requiring backup
|
||||
*/
|
||||
Crypto.prototype.countSessionsNeedingBackup = function() {
|
||||
return this._cryptoStore.countSessionsNeedingBackup();
|
||||
return this._backupManager.countSessionsNeedingBackup();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -3347,10 +2999,10 @@ Crypto.prototype._onRoomKeyEvent = function(event) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._checkedForBackup) {
|
||||
if (!this._backupManager.checkedForBackup) {
|
||||
// don't bother awaiting on this - the important thing is that we retry if we
|
||||
// haven't managed to check before
|
||||
this._checkAndStartKeyBackup();
|
||||
this._backupManager.checkAndStart();
|
||||
}
|
||||
|
||||
const alg = this._getRoomDecryptor(content.room_id, content.algorithm);
|
||||
|
||||
Reference in New Issue
Block a user