2022-01-20 11:16:08 +01:00
|
|
|
/*
|
|
|
|
Copyright 2020 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.
|
|
|
|
*/
|
|
|
|
|
2022-01-26 09:51:48 +01:00
|
|
|
import {StoreNames} from "../../../storage/common";
|
2022-01-27 16:07:18 +01:00
|
|
|
import {StoredRoomKey, keyFromBackup} from "../decryption/RoomKey";
|
2022-01-26 09:51:48 +01:00
|
|
|
import {MEGOLM_ALGORITHM} from "../../common";
|
2022-01-26 10:13:01 +01:00
|
|
|
import * as Curve25519 from "./Curve25519";
|
2022-01-26 15:19:31 +01:00
|
|
|
import {AbortableOperation} from "../../../../utils/AbortableOperation";
|
2022-01-28 13:13:23 +01:00
|
|
|
import {ObservableValue} from "../../../../observable/ObservableValue";
|
2022-01-26 09:51:48 +01:00
|
|
|
|
2022-01-26 15:19:31 +01:00
|
|
|
import {SetAbortableFn} from "../../../../utils/AbortableOperation";
|
|
|
|
import type {BackupInfo, SessionData, SessionKeyInfo, SessionInfo, KeyBackupPayload} from "./types";
|
2022-01-26 09:51:48 +01:00
|
|
|
import type {HomeServerApi} from "../../../net/HomeServerApi";
|
|
|
|
import type {IncomingRoomKey, RoomKey} from "../decryption/RoomKey";
|
|
|
|
import type {KeyLoader} from "../decryption/KeyLoader";
|
|
|
|
import type {SecretStorage} from "../../../ssss/SecretStorage";
|
|
|
|
import type {Storage} from "../../../storage/idb/Storage";
|
|
|
|
import type {ILogItem} from "../../../../logging/types";
|
|
|
|
import type {Platform} from "../../../../platform/web/Platform";
|
|
|
|
import type {Transaction} from "../../../storage/idb/Transaction";
|
2022-01-20 11:16:08 +01:00
|
|
|
import type * as OlmNamespace from "@matrix-org/olm";
|
|
|
|
type Olm = typeof OlmNamespace;
|
|
|
|
|
2022-01-28 13:13:23 +01:00
|
|
|
const KEYS_PER_REQUEST = 20;
|
|
|
|
|
2022-01-26 09:51:48 +01:00
|
|
|
export class KeyBackup {
|
2022-01-31 14:37:05 +01:00
|
|
|
public readonly operationInProgress = new ObservableValue<AbortableOperation<Promise<void>, Progress> | undefined>(undefined);
|
|
|
|
|
2022-01-31 16:26:14 +01:00
|
|
|
private _stopped = false;
|
2022-01-31 14:37:05 +01:00
|
|
|
private _needsNewKey = false;
|
2022-01-31 16:26:14 +01:00
|
|
|
private _hasBackedUpAllKeys = false;
|
2022-01-31 14:37:05 +01:00
|
|
|
private _error?: Error;
|
2022-01-28 13:13:23 +01:00
|
|
|
|
2022-01-20 11:16:08 +01:00
|
|
|
constructor(
|
|
|
|
private readonly backupInfo: BackupInfo,
|
2022-01-26 10:13:01 +01:00
|
|
|
private readonly crypto: Curve25519.BackupEncryption,
|
2022-01-20 11:16:08 +01:00
|
|
|
private readonly hsApi: HomeServerApi,
|
2022-01-25 18:48:19 +01:00
|
|
|
private readonly keyLoader: KeyLoader,
|
|
|
|
private readonly storage: Storage,
|
|
|
|
private readonly platform: Platform,
|
2022-01-20 11:16:08 +01:00
|
|
|
) {}
|
|
|
|
|
2022-01-31 16:26:14 +01:00
|
|
|
get hasStopped(): boolean { return this._stopped; }
|
2022-01-31 14:37:05 +01:00
|
|
|
get error(): Error | undefined { return this._error; }
|
2022-01-31 16:26:14 +01:00
|
|
|
get version(): string { return this.backupInfo.version; }
|
|
|
|
get needsNewKey(): boolean { return this._needsNewKey; }
|
|
|
|
get hasBackedUpAllKeys(): boolean { return this._hasBackedUpAllKeys; }
|
2022-01-31 14:37:05 +01:00
|
|
|
|
2022-01-25 18:48:19 +01:00
|
|
|
async getRoomKey(roomId: string, sessionId: string, log: ILogItem): Promise<IncomingRoomKey | undefined> {
|
2022-01-20 11:16:08 +01:00
|
|
|
const sessionResponse = await this.hsApi.roomKeyForRoomAndSession(this.backupInfo.version, roomId, sessionId, {log}).response();
|
2022-01-25 18:48:19 +01:00
|
|
|
if (!sessionResponse.session_data) {
|
|
|
|
return;
|
|
|
|
}
|
2022-01-26 15:19:31 +01:00
|
|
|
const sessionKeyInfo = this.crypto.decryptRoomKey(sessionResponse.session_data as SessionData);
|
2022-01-25 18:48:19 +01:00
|
|
|
if (sessionKeyInfo?.algorithm === MEGOLM_ALGORITHM) {
|
|
|
|
return keyFromBackup(roomId, sessionId, sessionKeyInfo);
|
|
|
|
} else if (sessionKeyInfo?.algorithm) {
|
|
|
|
log.set("unknown algorithm", sessionKeyInfo.algorithm);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-31 14:36:04 +01:00
|
|
|
markAllForBackup(txn: Transaction): Promise<number> {
|
|
|
|
return txn.inboundGroupSessions.markAllAsNotBackedUp();
|
|
|
|
}
|
|
|
|
|
2022-01-28 13:13:23 +01:00
|
|
|
flush(log: ILogItem): void {
|
|
|
|
if (!this.operationInProgress.get()) {
|
|
|
|
log.wrapDetached("flush key backup", async log => {
|
2022-01-31 14:37:05 +01:00
|
|
|
if (this._needsNewKey) {
|
|
|
|
log.set("needsNewKey", this._needsNewKey);
|
|
|
|
return;
|
|
|
|
}
|
2022-01-31 16:26:14 +01:00
|
|
|
this._stopped = false;
|
2022-01-31 14:37:05 +01:00
|
|
|
this._error = undefined;
|
2022-01-31 16:26:14 +01:00
|
|
|
this._hasBackedUpAllKeys = false;
|
2022-01-31 14:37:05 +01:00
|
|
|
const operation = this._runFlushOperation(log);
|
2022-01-28 13:13:23 +01:00
|
|
|
this.operationInProgress.set(operation);
|
|
|
|
try {
|
2022-01-31 14:37:05 +01:00
|
|
|
await operation.result;
|
2022-01-31 16:26:14 +01:00
|
|
|
this._hasBackedUpAllKeys = true;
|
2022-01-28 13:13:23 +01:00
|
|
|
} catch (err) {
|
2022-01-31 16:26:14 +01:00
|
|
|
this._stopped = true;
|
2022-01-31 14:37:05 +01:00
|
|
|
if (err.name === "HomeServerError" && err.errcode === "M_WRONG_ROOM_KEYS_VERSION") {
|
|
|
|
log.set("wrong_version", true);
|
|
|
|
this._needsNewKey = true;
|
|
|
|
} else {
|
|
|
|
// TODO should really also use AbortError in storage
|
|
|
|
if (err.name !== "AbortError" || (err.name === "StorageError" && err.errcode === "AbortError")) {
|
|
|
|
this._error = err;
|
|
|
|
}
|
|
|
|
}
|
2022-01-28 13:13:23 +01:00
|
|
|
log.catch(err);
|
|
|
|
}
|
|
|
|
this.operationInProgress.set(undefined);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-31 14:37:05 +01:00
|
|
|
private _runFlushOperation(log: ILogItem): AbortableOperation<Promise<void>, Progress> {
|
2022-01-26 15:19:31 +01:00
|
|
|
return new AbortableOperation(async (setAbortable, setProgress) => {
|
|
|
|
let total = 0;
|
|
|
|
let amountFinished = 0;
|
|
|
|
while (true) {
|
2022-01-28 13:13:23 +01:00
|
|
|
const waitMs = this.platform.random() * 10000;
|
|
|
|
const timeout = this.platform.clock.createTimeout(waitMs);
|
2022-01-26 15:19:31 +01:00
|
|
|
setAbortable(timeout);
|
|
|
|
await timeout.elapsed();
|
2022-01-27 16:07:18 +01:00
|
|
|
const txn = await this.storage.readTxn([StoreNames.inboundGroupSessions]);
|
2022-01-26 15:19:31 +01:00
|
|
|
setAbortable(txn);
|
|
|
|
// fetch total again on each iteration as while we are flushing, sync might be adding keys
|
2022-01-31 16:23:48 +01:00
|
|
|
total = amountFinished + await txn.inboundGroupSessions.countNonBackedUpSessions();
|
2022-01-26 15:19:31 +01:00
|
|
|
setProgress(new Progress(total, amountFinished));
|
2022-01-28 13:13:23 +01:00
|
|
|
const keysNeedingBackup = (await txn.inboundGroupSessions.getFirstNonBackedUpSessions(KEYS_PER_REQUEST))
|
2022-01-27 16:07:18 +01:00
|
|
|
.map(entry => new StoredRoomKey(entry));
|
2022-01-26 15:19:31 +01:00
|
|
|
if (keysNeedingBackup.length === 0) {
|
2022-01-31 14:37:05 +01:00
|
|
|
return;
|
2022-01-25 18:48:19 +01:00
|
|
|
}
|
2022-01-27 16:07:18 +01:00
|
|
|
const payload = await this.encodeKeysForBackup(keysNeedingBackup);
|
|
|
|
const uploadRequest = this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload, {log});
|
|
|
|
setAbortable(uploadRequest);
|
2022-01-31 14:37:05 +01:00
|
|
|
await uploadRequest.response();
|
2022-01-28 13:11:52 +01:00
|
|
|
await this.markKeysAsBackedUp(keysNeedingBackup, setAbortable);
|
2022-01-26 15:19:31 +01:00
|
|
|
amountFinished += keysNeedingBackup.length;
|
|
|
|
setProgress(new Progress(total, amountFinished));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private async encodeKeysForBackup(roomKeys: RoomKey[]): Promise<KeyBackupPayload> {
|
|
|
|
const payload: KeyBackupPayload = { rooms: {} };
|
|
|
|
const payloadRooms = payload.rooms;
|
|
|
|
for (const key of roomKeys) {
|
|
|
|
let roomPayload = payloadRooms[key.roomId];
|
|
|
|
if (!roomPayload) {
|
|
|
|
roomPayload = payloadRooms[key.roomId] = { sessions: {} };
|
2022-01-25 18:48:19 +01:00
|
|
|
}
|
2022-01-26 15:19:31 +01:00
|
|
|
roomPayload.sessions[key.sessionId] = await this.encodeRoomKey(key);
|
2022-01-25 18:48:19 +01:00
|
|
|
}
|
2022-01-26 15:19:31 +01:00
|
|
|
return payload;
|
|
|
|
}
|
|
|
|
|
2022-01-27 16:07:18 +01:00
|
|
|
private async markKeysAsBackedUp(roomKeys: RoomKey[], setAbortable: SetAbortableFn) {
|
2022-01-26 15:19:31 +01:00
|
|
|
const txn = await this.storage.readWriteTxn([
|
2022-01-27 16:07:18 +01:00
|
|
|
StoreNames.inboundGroupSessions,
|
2022-01-26 15:19:31 +01:00
|
|
|
]);
|
|
|
|
setAbortable(txn);
|
|
|
|
try {
|
2022-01-28 13:13:23 +01:00
|
|
|
await Promise.all(roomKeys.map(key => {
|
|
|
|
return txn.inboundGroupSessions.markAsBackedUp(key.roomId, key.senderKey, key.sessionId);
|
|
|
|
}));
|
2022-01-26 15:19:31 +01:00
|
|
|
} catch (err) {
|
|
|
|
txn.abort();
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
await txn.complete();
|
2022-01-25 18:48:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private async encodeRoomKey(roomKey: RoomKey): Promise<SessionInfo> {
|
|
|
|
return await this.keyLoader.useKey(roomKey, session => {
|
|
|
|
const firstMessageIndex = session.first_known_index();
|
|
|
|
const sessionKey = session.export_session(firstMessageIndex);
|
|
|
|
return {
|
|
|
|
first_message_index: firstMessageIndex,
|
|
|
|
forwarded_count: 0,
|
|
|
|
is_verified: false,
|
2022-01-26 10:13:01 +01:00
|
|
|
session_data: this.crypto.encryptRoomKey(roomKey, sessionKey)
|
2022-01-25 18:48:19 +01:00
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-01-20 11:16:08 +01:00
|
|
|
dispose() {
|
2022-01-26 10:13:01 +01:00
|
|
|
this.crypto.dispose();
|
2022-01-20 11:16:08 +01:00
|
|
|
}
|
|
|
|
|
2022-01-26 09:51:48 +01:00
|
|
|
static async fromSecretStorage(platform: Platform, olm: Olm, secretStorage: SecretStorage, hsApi: HomeServerApi, keyLoader: KeyLoader, storage: Storage, txn: Transaction): Promise<KeyBackup | undefined> {
|
2022-01-20 11:16:08 +01:00
|
|
|
const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1", txn);
|
|
|
|
if (base64PrivateKey) {
|
|
|
|
const privateKey = new Uint8Array(platform.encoding.base64.decode(base64PrivateKey));
|
|
|
|
const backupInfo = await hsApi.roomKeysVersion().response() as BackupInfo;
|
2022-01-26 10:13:01 +01:00
|
|
|
if (backupInfo.algorithm === Curve25519.Algorithm) {
|
|
|
|
const crypto = Curve25519.BackupEncryption.fromAuthData(backupInfo.auth_data, privateKey, olm);
|
|
|
|
return new KeyBackup(backupInfo, crypto, hsApi, keyLoader, storage, platform);
|
2022-01-20 11:16:08 +01:00
|
|
|
} else {
|
|
|
|
throw new Error(`Unknown backup algorithm: ${backupInfo.algorithm}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-01-26 15:19:31 +01:00
|
|
|
|
|
|
|
export class Progress {
|
|
|
|
constructor(
|
|
|
|
public readonly total: number,
|
|
|
|
public readonly finished: number
|
|
|
|
) {}
|
|
|
|
}
|