You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-29 16:43:09 +03:00
factor out backup management to a separate module
This commit is contained in:
155
src/client.js
155
src/client.js
@@ -2,7 +2,7 @@
|
|||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2018-2019 New Vector 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 { User } from "./models/user";
|
||||||
import {AutoDiscovery} from "./autodiscovery";
|
import {AutoDiscovery} from "./autodiscovery";
|
||||||
import {DEHYDRATION_ALGORITHM} from "./crypto/dehydration";
|
import {DEHYDRATION_ALGORITHM} from "./crypto/dehydration";
|
||||||
|
import { BackupManager } from "./crypto/backup";
|
||||||
|
|
||||||
const SCROLLBACK_DELAY_MS = 3000;
|
const SCROLLBACK_DELAY_MS = 3000;
|
||||||
export const CRYPTO_ENABLED = isCryptoAvailable();
|
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.
|
* {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]
|
* @param {function} [opts.cryptoCallbacks.cacheSecretStorageKey]
|
||||||
* Optional. Function called when a new encryption key for secret storage
|
* 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
|
* 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
|
// check the key backup status, since whether or not we use this depends on
|
||||||
// whether it has a signature from a verified device
|
// whether it has a signature from a verified device
|
||||||
if (userId == this.credentials.userId) {
|
if (userId == this.credentials.userId) {
|
||||||
this._crypto.checkKeyBackup();
|
this.checkKeyBackup();
|
||||||
}
|
}
|
||||||
return prom;
|
return prom;
|
||||||
};
|
};
|
||||||
@@ -1776,7 +1781,7 @@ MatrixClient.prototype.importRoomKeys = function(keys, opts) {
|
|||||||
* in trustInfo.
|
* in trustInfo.
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.checkKeyBackup = function() {
|
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) {
|
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) {
|
if (this._crypto === null) {
|
||||||
throw new Error("End-to-end encryption disabled");
|
throw new Error("End-to-end encryption disabled");
|
||||||
}
|
}
|
||||||
if (!this._crypto._checkedForBackup) {
|
return this._crypto._backupManager.getKeyBackupEnabled();
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return Boolean(this._crypto.backupKey);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1847,16 +1849,7 @@ MatrixClient.prototype.enableKeyBackup = function(info) {
|
|||||||
throw new Error("End-to-end encryption disabled");
|
throw new Error("End-to-end encryption disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
this._crypto.backupInfo = info;
|
return this._crypto.backupManager.enableKeyBackup();
|
||||||
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();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1867,11 +1860,7 @@ MatrixClient.prototype.disableKeyBackup = function() {
|
|||||||
throw new Error("End-to-end encryption disabled");
|
throw new Error("End-to-end encryption disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
this._crypto.backupInfo = null;
|
this._crypto.backupManager.disableKeyBackup();
|
||||||
if (this._crypto.backupKey) this._crypto.backupKey.free();
|
|
||||||
this._crypto.backupKey = null;
|
|
||||||
|
|
||||||
this.emit('crypto.keyBackupStatus', false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1896,26 +1885,19 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(
|
|||||||
throw new Error("End-to-end encryption disabled");
|
throw new Error("End-to-end encryption disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { keyInfo, encodedPrivateKey, privateKey } =
|
// eslint-disable-next-line camelcase
|
||||||
await this.createRecoveryKeyFromPassphrase(password);
|
const {algorithm, auth_data, recovery_key, privateKey} =
|
||||||
|
await this._crypto._backupManager.prepareKeyBackupVersion(password);
|
||||||
|
|
||||||
if (secureSecretStorage) {
|
if (secureSecretStorage) {
|
||||||
await this.storeSecret("m.megolm_backup.v1", encodeBase64(privateKey));
|
await this.storeSecret("m.megolm_backup.v1", encodeBase64(privateKey));
|
||||||
logger.info("Key backup private key stored in secret storage");
|
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 {
|
return {
|
||||||
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
|
algorithm,
|
||||||
auth_data: authData,
|
auth_data,
|
||||||
recovery_key: encodedPrivateKey,
|
recovery_key,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1941,6 +1923,8 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info) {
|
|||||||
throw new Error("End-to-end encryption disabled");
|
throw new Error("End-to-end encryption disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this._crypto._backupManager.createKeyBackupVersion(info);
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
algorithm: info.algorithm,
|
algorithm: info.algorithm,
|
||||||
auth_data: info.auth_data,
|
auth_data: info.auth_data,
|
||||||
@@ -1986,8 +1970,8 @@ MatrixClient.prototype.deleteKeyBackupVersion = function(version) {
|
|||||||
// If we're currently backing up to this backup... stop.
|
// If we're currently backing up to this backup... stop.
|
||||||
// (We start using it automatically in createKeyBackupVersion
|
// (We start using it automatically in createKeyBackupVersion
|
||||||
// so this is symmetrical).
|
// so this is symmetrical).
|
||||||
if (this._crypto.backupInfo && this._crypto.backupInfo.version === version) {
|
if (this._crypto._backupManager.version) {
|
||||||
this.disableKeyBackup();
|
this._crypto._backupManager.disableKeyBackup();
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = utils.encodeUri("/room_keys/version/$version", {
|
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");
|
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");
|
throw new Error("End-to-end encryption disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._crypto.flagAllGroupSessionsForBackup();
|
return this._crypto._backupManager.flagAllGroupSessionsForBackup();
|
||||||
};
|
};
|
||||||
|
|
||||||
MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
|
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,
|
privKey, targetRoomId, targetSessionId, backupInfo,
|
||||||
{
|
{
|
||||||
cacheCompleteCallback, // For sequencing during tests
|
cacheCompleteCallback, // For sequencing during tests
|
||||||
@@ -2225,46 +2209,41 @@ MatrixClient.prototype._restoreKeyBackup = function(
|
|||||||
targetRoomId, targetSessionId, backupInfo.version,
|
targetRoomId, targetSessionId, backupInfo.version,
|
||||||
);
|
);
|
||||||
|
|
||||||
const decryption = new global.Olm.PkDecryption();
|
const algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => { return privKey; });
|
||||||
let backupPubKey;
|
|
||||||
try {
|
try {
|
||||||
backupPubKey = decryption.init_with_private_key(privKey);
|
// If the pubkey computed from the private data we've been given
|
||||||
} catch (e) {
|
// doesn't match the one in the auth_data, the user has entered
|
||||||
decryption.free();
|
// a different recovery key / the wrong passphrase.
|
||||||
throw e;
|
if (!await algorithm.keyMatches(privKey)) {
|
||||||
}
|
return Promise.reject({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY });
|
||||||
|
}
|
||||||
|
|
||||||
// If the pubkey computed from the private data we've been given
|
// Cache the key, if possible.
|
||||||
// doesn't match the one in the auth_data, the user has enetered
|
// This is async.
|
||||||
// a different recovery key / the wrong passphrase.
|
this._crypto.storeSessionBackupPrivateKey(privKey)
|
||||||
if (backupPubKey !== backupInfo.auth_data.public_key) {
|
.catch((e) => {
|
||||||
return Promise.reject({errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY});
|
logger.warn("Error caching session backup key:", e);
|
||||||
}
|
}).then(cacheCompleteCallback);
|
||||||
|
|
||||||
// Cache the key, if possible.
|
if (progressCallback) {
|
||||||
// This is async.
|
progressCallback({
|
||||||
this._crypto.storeSessionBackupPrivateKey(privKey)
|
stage: "fetch",
|
||||||
.catch((e) => {
|
});
|
||||||
logger.warn("Error caching session backup key:", e);
|
}
|
||||||
}).then(cacheCompleteCallback);
|
|
||||||
|
|
||||||
if (progressCallback) {
|
const res = await this._http.authedRequest(
|
||||||
progressCallback({
|
undefined, "GET", path.path, path.queryData, undefined,
|
||||||
stage: "fetch",
|
{ prefix: PREFIX_UNSTABLE },
|
||||||
});
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return this._http.authedRequest(
|
|
||||||
undefined, "GET", path.path, path.queryData, undefined,
|
|
||||||
{prefix: PREFIX_UNSTABLE},
|
|
||||||
).then((res) => {
|
|
||||||
if (res.rooms) {
|
if (res.rooms) {
|
||||||
for (const [roomId, roomData] of Object.entries(res.rooms)) {
|
for (const [roomId, roomData] of Object.entries(res.rooms)) {
|
||||||
if (!roomData.sessions) continue;
|
if (!roomData.sessions) continue;
|
||||||
|
|
||||||
totalKeyCount += Object.keys(roomData.sessions).length;
|
totalKeyCount += Object.keys(roomData.sessions).length;
|
||||||
const roomKeys = keysFromRecoverySession(
|
const roomKeys = await algorithm.decryptSessions(
|
||||||
roomData.sessions, decryption, roomId,
|
roomData.sessions,
|
||||||
);
|
);
|
||||||
for (const k of roomKeys) {
|
for (const k of roomKeys) {
|
||||||
k.room_id = roomId;
|
k.room_id = roomId;
|
||||||
@@ -2273,13 +2252,18 @@ MatrixClient.prototype._restoreKeyBackup = function(
|
|||||||
}
|
}
|
||||||
} else if (res.sessions) {
|
} else if (res.sessions) {
|
||||||
totalKeyCount = Object.keys(res.sessions).length;
|
totalKeyCount = Object.keys(res.sessions).length;
|
||||||
keys = keysFromRecoverySession(
|
keys = await algorithm.decryptSessions(
|
||||||
res.sessions, decryption, targetRoomId, keys,
|
res.sessions,
|
||||||
);
|
);
|
||||||
|
for (const k of keys) {
|
||||||
|
k.room_id = targetRoomId;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
totalKeyCount = 1;
|
totalKeyCount = 1;
|
||||||
try {
|
try {
|
||||||
const key = keyFromRecoverySession(res, decryption);
|
const [key] = await algorithm.decryptSessions({
|
||||||
|
[targetSessionId]: res,
|
||||||
|
});
|
||||||
key.room_id = targetRoomId;
|
key.room_id = targetRoomId;
|
||||||
key.session_id = targetSessionId;
|
key.session_id = targetSessionId;
|
||||||
keys.push(key);
|
keys.push(key);
|
||||||
@@ -2287,19 +2271,20 @@ MatrixClient.prototype._restoreKeyBackup = function(
|
|||||||
logger.log("Failed to decrypt megolm session from backup", e);
|
logger.log("Failed to decrypt megolm session from backup", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
algorithm.free();
|
||||||
|
}
|
||||||
|
|
||||||
return this.importRoomKeys(keys, {
|
await this.importRoomKeys(keys, {
|
||||||
progressCallback,
|
progressCallback,
|
||||||
untrusted: true,
|
untrusted: true,
|
||||||
source: "backup",
|
source: "backup",
|
||||||
});
|
|
||||||
}).then(() => {
|
|
||||||
return this._crypto.setTrustedBackupPubKey(backupPubKey);
|
|
||||||
}).then(() => {
|
|
||||||
return {total: totalKeyCount, imported: keys.length};
|
|
||||||
}).finally(() => {
|
|
||||||
decryption.free();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// await this._crypto.setTrustedBackupPubKey(backupPubKey);
|
||||||
|
await this.checkKeyBackup();
|
||||||
|
|
||||||
|
return {total: totalKeyCount, imported: keys.length};
|
||||||
};
|
};
|
||||||
|
|
||||||
MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, version) {
|
MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, version) {
|
||||||
|
|||||||
@@ -415,9 +415,8 @@ MegolmEncryption.prototype._prepareNewSession = async function(sharedHistory) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// don't wait for it to complete
|
// don't wait for it to complete
|
||||||
this._crypto.backupGroupSession(
|
this._crypto._backupManager.backupGroupSession(
|
||||||
this._roomId, this._olmDevice.deviceCurve25519Key, [],
|
this._olmDevice.deviceCurve25519Key, sessionId,
|
||||||
sessionId, key.key,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return new OutboundSessionInfo(sessionId, sharedHistory);
|
return new OutboundSessionInfo(sessionId, sharedHistory);
|
||||||
@@ -1428,11 +1427,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
|||||||
});
|
});
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// don't wait for the keys to be backed up for the server
|
// don't wait for the keys to be backed up for the server
|
||||||
this._crypto.backupGroupSession(
|
this._crypto._backupManager.backupGroupSession(senderKey, content.session_id);
|
||||||
content.room_id, senderKey, forwardingKeyChain,
|
|
||||||
content.session_id, content.session_key, keysClaimed,
|
|
||||||
exportFormat,
|
|
||||||
);
|
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
logger.error(`Error handling m.room_key_event: ${e}`);
|
logger.error(`Error handling m.room_key_event: ${e}`);
|
||||||
});
|
});
|
||||||
@@ -1648,14 +1643,8 @@ MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
|
|||||||
).then(() => {
|
).then(() => {
|
||||||
if (opts.source !== "backup") {
|
if (opts.source !== "backup") {
|
||||||
// don't wait for it to complete
|
// don't wait for it to complete
|
||||||
this._crypto.backupGroupSession(
|
this._crypto._backupManager.backupGroupSession(
|
||||||
session.room_id,
|
session.sender_key, session.session_id,
|
||||||
session.sender_key,
|
|
||||||
session.forwarding_curve25519_key_chain,
|
|
||||||
session.session_id,
|
|
||||||
session.session_key,
|
|
||||||
session.sender_claimed_keys,
|
|
||||||
true,
|
|
||||||
).catch((e) => {
|
).catch((e) => {
|
||||||
// This throws if the upload failed, but this is fine
|
// This throws if the upload failed, but this is fine
|
||||||
// since it will have written it to the db and will retry.
|
// 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 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2018-2019 New Vector 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 {decryptAES, encryptAES} from './aes';
|
||||||
import {DehydrationManager} from './dehydration';
|
import {DehydrationManager} from './dehydration';
|
||||||
import { MatrixEvent } from "../models/event";
|
import { MatrixEvent } from "../models/event";
|
||||||
|
import { BackupManager } from "./backup";
|
||||||
|
|
||||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||||
|
|
||||||
@@ -85,7 +86,6 @@ export function isCryptoAvailable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
|
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
|
||||||
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cryptography bits
|
* Cryptography bits
|
||||||
@@ -154,13 +154,36 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
|
|||||||
} else {
|
} else {
|
||||||
this._verificationMethods = defaultVerificationMethods;
|
this._verificationMethods = defaultVerificationMethods;
|
||||||
}
|
}
|
||||||
// track whether this device's megolm keys are being backed up incrementally
|
|
||||||
// to the server or not.
|
this._backupManager = new BackupManager(baseApis, async (algorithm) => {
|
||||||
// XXX: this should probably have a single source of truth from OlmAccount
|
// try to get key from cache
|
||||||
this.backupInfo = null; // The info dict from /room_keys/version
|
const cachedKey = await this.getSessionBackupPrivateKey();
|
||||||
this.backupKey = null; // The encryption key object
|
if (cachedKey) {
|
||||||
this._checkedForBackup = false; // Have we checked the server for a backup we can use?
|
return cachedKey;
|
||||||
this._sendingBackups = false; // Are we currently sending backups?
|
}
|
||||||
|
|
||||||
|
// 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._olmDevice = new OlmDevice(cryptoStore);
|
||||||
this._deviceList = new DeviceList(
|
this._deviceList = new DeviceList(
|
||||||
@@ -331,7 +354,7 @@ Crypto.prototype.init = async function(opts) {
|
|||||||
this._deviceList.startTrackingDeviceList(this._userId);
|
this._deviceList.startTrackingDeviceList(this._userId);
|
||||||
|
|
||||||
logger.log("Crypto: checking for key backup...");
|
logger.log("Crypto: checking for key backup...");
|
||||||
this._checkAndStartKeyBackup();
|
this._backupManager.checkAndStart();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -458,7 +481,7 @@ Crypto.prototype.isSecretStorageReady = async function() {
|
|||||||
this._secretStorage,
|
this._secretStorage,
|
||||||
);
|
);
|
||||||
const sessionBackupInStorage = (
|
const sessionBackupInStorage = (
|
||||||
!this._baseApis.getKeyBackupEnabled() ||
|
!this._backupManager.getKeyBackupEnabled() ||
|
||||||
this._baseApis.isKeyBackupKeyStored()
|
this._baseApis.isKeyBackupKeyStored()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -522,9 +545,11 @@ Crypto.prototype.bootstrapCrossSigning = async function({
|
|||||||
builder.addKeySignature(this._userId, this._deviceId, deviceSignature);
|
builder.addKeySignature(this._userId, this._deviceId, deviceSignature);
|
||||||
|
|
||||||
// Sign message key backup with cross-signing master key
|
// Sign message key backup with cross-signing master key
|
||||||
if (this.backupInfo) {
|
if (this._backupManager.backupInfo) {
|
||||||
await crossSigningInfo.signObject(this.backupInfo.auth_data, "master");
|
await crossSigningInfo.signObject(
|
||||||
builder.addSessionBackup(this.backupInfo);
|
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_salt &&
|
||||||
keyBackupInfo.auth_data.private_key_iterations
|
keyBackupInfo.auth_data.private_key_iterations
|
||||||
) {
|
) {
|
||||||
|
// FIXME: ???
|
||||||
opts.passphrase = {
|
opts.passphrase = {
|
||||||
algorithm: "m.pbkdf2",
|
algorithm: "m.pbkdf2",
|
||||||
iterations: keyBackupInfo.auth_data.private_key_iterations,
|
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
|
// 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
|
// FIXME: if we previously trusted the backup, should we automatically sign
|
||||||
// the backup with the new key (if not already signed)?
|
// 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}`);
|
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) {
|
Crypto.prototype.setTrustedBackupPubKey = async function(trustedPubKey) {
|
||||||
// This should be redundant post cross-signing is a thing, so just
|
// This should be redundant post cross-signing is a thing, so just
|
||||||
// plonk it in localStorage for now.
|
// plonk it in localStorage for now.
|
||||||
this._sessionStore.setLocalTrustedBackupPubKey(trustedPubKey);
|
this._sessionStore.setLocalTrustedBackupPubKey(trustedPubKey);
|
||||||
await this.checkKeyBackup();
|
await this._backupManager.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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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
|
* 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
|
* @returns {Promise<int>} Resolves to the number of sessions requiring backup
|
||||||
*/
|
*/
|
||||||
Crypto.prototype.countSessionsNeedingBackup = function() {
|
Crypto.prototype.countSessionsNeedingBackup = function() {
|
||||||
return this._cryptoStore.countSessionsNeedingBackup();
|
return this._backupManager.countSessionsNeedingBackup();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3347,10 +2999,10 @@ Crypto.prototype._onRoomKeyEvent = function(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._checkedForBackup) {
|
if (!this._backupManager.checkedForBackup) {
|
||||||
// don't bother awaiting on this - the important thing is that we retry if we
|
// don't bother awaiting on this - the important thing is that we retry if we
|
||||||
// haven't managed to check before
|
// haven't managed to check before
|
||||||
this._checkAndStartKeyBackup();
|
this._backupManager.checkAndStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
const alg = this._getRoomDecryptor(content.room_id, content.algorithm);
|
const alg = this._getRoomDecryptor(content.room_id, content.algorithm);
|
||||||
|
|||||||
Reference in New Issue
Block a user