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-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-26 09:51:48 +01:00
|
|
|
export class KeyBackup {
|
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-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-26 15:19:31 +01:00
|
|
|
// TODO: protect against having multiple concurrent flushes
|
2022-01-27 15:14:29 +01:00
|
|
|
flush(log: ILogItem): AbortableOperation<Promise<boolean>, Progress> {
|
2022-01-26 15:19:31 +01:00
|
|
|
return new AbortableOperation(async (setAbortable, setProgress) => {
|
|
|
|
let total = 0;
|
|
|
|
let amountFinished = 0;
|
|
|
|
while (true) {
|
|
|
|
const timeout = this.platform.clock.createTimeout(this.platform.random() * 10000);
|
|
|
|
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-27 16:07:18 +01:00
|
|
|
total = await txn.inboundGroupSessions.countNonBackedUpSessions();
|
2022-01-26 15:19:31 +01:00
|
|
|
setProgress(new Progress(total, amountFinished));
|
2022-01-27 16:07:18 +01:00
|
|
|
const keysNeedingBackup = (await txn.inboundGroupSessions.getFirstNonBackedUpSessions(20))
|
|
|
|
.map(entry => new StoredRoomKey(entry));
|
2022-01-26 15:19:31 +01:00
|
|
|
if (keysNeedingBackup.length === 0) {
|
2022-01-27 15:14:29 +01:00
|
|
|
return true;
|
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);
|
|
|
|
try {
|
|
|
|
await uploadRequest.response();
|
|
|
|
} catch (err) {
|
|
|
|
if (err.name === "HomeServerError" && err.errcode === "M_WRONG_ROOM_KEYS_VERSION") {
|
|
|
|
log.set("wrong_version", true);
|
|
|
|
return false;
|
|
|
|
} else {
|
|
|
|
throw err;
|
2022-01-27 15:14:29 +01:00
|
|
|
}
|
2022-01-26 15:19:31 +01:00
|
|
|
}
|
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-27 16:07:18 +01:00
|
|
|
await Promise.all(roomKeys.map(key => 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
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
get version(): string {
|
2022-01-20 11:16:08 +01:00
|
|
|
return this.backupInfo.version;
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
) {}
|
|
|
|
}
|