This commit is contained in:
Bruno Windels 2023-03-21 18:24:46 +01:00
parent 780dfeb199
commit dd59f37dce
12 changed files with 473 additions and 312 deletions

View File

@ -16,7 +16,7 @@ limitations under the License.
import {ViewModel} from "./ViewModel";
import {KeyType} from "../matrix/ssss/index";
import {Status} from "./session/settings/KeyBackupViewModel.js";
import {Status} from "./session/settings/KeyBackupViewModel";
export class AccountSetupViewModel extends ViewModel {
constructor(options) {

View File

@ -1,223 +0,0 @@
/*
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.
*/
import {ViewModel} from "../../ViewModel";
import {KeyType} from "../../../matrix/ssss/index";
import {createEnum} from "../../../utils/enum";
import {FlatMapObservableValue} from "../../../observable/value";
export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending", "NewVersionAvailable");
export const BackupWriteStatus = createEnum("Writing", "Stopped", "Done", "Pending");
export class KeyBackupViewModel extends ViewModel {
constructor(options) {
super(options);
this._session = options.session;
this._error = null;
this._isBusy = false;
this._dehydratedDeviceId = undefined;
this._status = undefined;
this._backupOperation = new FlatMapObservableValue(this._session.keyBackup, keyBackup => keyBackup.operationInProgress);
this._progress = new FlatMapObservableValue(this._backupOperation, op => op.progress);
this.track(this._backupOperation.subscribe(() => {
// see if needsNewKey might be set
this._reevaluateStatus();
this.emitChange("isBackingUp");
}));
this.track(this._progress.subscribe(() => this.emitChange("backupPercentage")));
this._reevaluateStatus();
this.track(this._session.keyBackup.subscribe(() => {
if (this._reevaluateStatus()) {
this.emitChange("status");
}
}));
}
_reevaluateStatus() {
if (this._isBusy) {
return false;
}
let status;
const keyBackup = this._session.keyBackup.get();
if (keyBackup) {
status = keyBackup.needsNewKey ? Status.NewVersionAvailable : Status.Enabled;
} else if (keyBackup === null) {
status = this.showPhraseSetup() ? Status.SetupPhrase : Status.SetupKey;
} else {
status = Status.Pending;
}
const changed = status !== this._status;
this._status = status;
return changed;
}
get decryptAction() {
return this.i18n`Set up`;
}
get purpose() {
return this.i18n`set up key backup`;
}
offerDehydratedDeviceSetup() {
return true;
}
get dehydratedDeviceId() {
return this._dehydratedDeviceId;
}
get isBusy() {
return this._isBusy;
}
get backupVersion() {
return this._session.keyBackup.get()?.version;
}
get isMasterKeyTrusted() {
return this._session.crossSigning?.isMasterKeyTrusted ?? false;
}
get canSignOwnDevice() {
return !!this._session.crossSigning;
}
async signOwnDevice() {
if (this._session.crossSigning) {
await this.logger.run("KeyBackupViewModel.signOwnDevice", async log => {
await this._session.crossSigning.signOwnDevice(log);
});
}
}
get backupWriteStatus() {
const keyBackup = this._session.keyBackup.get();
if (!keyBackup) {
return BackupWriteStatus.Pending;
} else if (keyBackup.hasStopped) {
return BackupWriteStatus.Stopped;
}
const operation = keyBackup.operationInProgress.get();
if (operation) {
return BackupWriteStatus.Writing;
} else if (keyBackup.hasBackedUpAllKeys) {
return BackupWriteStatus.Done;
} else {
return BackupWriteStatus.Pending;
}
}
get backupError() {
return this._session.keyBackup.get()?.error?.message;
}
get status() {
return this._status;
}
get error() {
return this._error?.message;
}
showPhraseSetup() {
if (this._status === Status.SetupKey) {
this._status = Status.SetupPhrase;
this.emitChange("status");
}
}
showKeySetup() {
if (this._status === Status.SetupPhrase) {
this._status = Status.SetupKey;
this.emitChange("status");
}
}
async _enterCredentials(keyType, credential, setupDehydratedDevice) {
if (credential) {
try {
this._isBusy = true;
this.emitChange("isBusy");
const key = await this._session.enableSecretStorage(keyType, credential);
if (setupDehydratedDevice) {
this._dehydratedDeviceId = await this._session.setupDehydratedDevice(key);
}
} catch (err) {
console.error(err);
this._error = err;
this.emitChange("error");
} finally {
this._isBusy = false;
this._reevaluateStatus();
this.emitChange("");
}
}
}
enterSecurityPhrase(passphrase, setupDehydratedDevice) {
this._enterCredentials(KeyType.Passphrase, passphrase, setupDehydratedDevice);
}
enterSecurityKey(securityKey, setupDehydratedDevice) {
this._enterCredentials(KeyType.RecoveryKey, securityKey, setupDehydratedDevice);
}
async disable() {
try {
this._isBusy = true;
this.emitChange("isBusy");
await this._session.disableSecretStorage();
} catch (err) {
console.error(err);
this._error = err;
this.emitChange("error");
} finally {
this._isBusy = false;
this._reevaluateStatus();
this.emitChange("");
}
}
get isBackingUp() {
return !!this._backupOperation.get();
}
get backupPercentage() {
const progress = this._progress.get();
if (progress) {
return Math.round((progress.finished / progress.total) * 100);
}
return 0;
}
get backupInProgressLabel() {
const progress = this._progress.get();
if (progress) {
return this.i18n`${progress.finished} of ${progress.total}`;
}
return this.i18n``;
}
cancelBackup() {
this._backupOperation.get()?.abort();
}
startBackup() {
this._session.keyBackup.get()?.flush();
}
}

View File

@ -0,0 +1,270 @@
/*
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.
*/
import {ViewModel} from "../../ViewModel";
import {SegmentType} from "../../navigation/index";
import {KeyType} from "../../../matrix/ssss/index";
import type {Options as BaseOptions} from "../../ViewModel";
import type {Session} from "../../../matrix/Session";
import type {Disposable} from "../../../utils/Disposables";
import type {KeyBackup, Progress} from "../../../matrix/e2ee/megolm/keybackup/KeyBackup";
import type {CrossSigning} from "../../../matrix/verification/CrossSigning";
export enum Status {
Enabled,
Setup,
Pending,
NewVersionAvailable
};
export enum BackupWriteStatus {
Writing,
Stopped,
Done,
Pending
};
type Options = {
session: Session,
} & BaseOptions;
export class KeyBackupViewModel extends ViewModel<SegmentType, Options> {
private _error?: Error = undefined;
private _isBusy = false;
private _dehydratedDeviceId?: string = undefined;
private _status = Status.Pending;
private _backupOperationSubscription?: Disposable = undefined;
private _keyBackupSubscription?: Disposable = undefined;
private _progress?: Progress = undefined;
private _setupKeyType = KeyType.RecoveryKey;
constructor(options) {
super(options);
const onKeyBackupSet = (keyBackup: KeyBackup | undefined) => {
if (keyBackup && !this._keyBackupSubscription) {
this._keyBackupSubscription = this.track(this._session.keyBackup.disposableOn("change", () => {
this._onKeyBackupChange();
}));
} else if (!keyBackup && this._keyBackupSubscription) {
this._keyBackupSubscription = this.disposeTracked(this._keyBackupSubscription);
}
this._onKeyBackupChange(); // update status
};
this.track(this._session.keyBackup.subscribe(onKeyBackupSet));
onKeyBackupSet(this._keyBackup);
}
private get _session(): Session {
return this.getOption("session");
}
private get _keyBackup(): KeyBackup | undefined {
return this._session.keyBackup.get();
}
private get _crossSigning(): CrossSigning | undefined {
return this._session.crossSigning.get();
}
private _onKeyBackupChange() {
const keyBackup = this._keyBackup;
if (keyBackup) {
const {operationInProgress} = keyBackup;
if (operationInProgress && !this._backupOperationSubscription) {
this._backupOperationSubscription = this.track(operationInProgress.disposableOn("change", () => {
this._progress = operationInProgress.progress;
this.emitChange("backupPercentage");
}));
} else if (this._backupOperationSubscription && !operationInProgress) {
this._backupOperationSubscription = this.disposeTracked(this._backupOperationSubscription);
this._progress = undefined;
}
}
this.emitChange("status");
}
get status(): Status {
const keyBackup = this._keyBackup;
if (keyBackup) {
if (keyBackup.needsNewKey) {
return Status.NewVersionAvailable;
} else if (keyBackup.version === undefined) {
return Status.Pending;
} else {
return keyBackup.needsNewKey ? Status.NewVersionAvailable : Status.Enabled;
}
} else {
return Status.Setup;
}
}
get decryptAction(): string {
return this.i18n`Set up`;
}
get purpose(): string {
return this.i18n`set up key backup`;
}
offerDehydratedDeviceSetup(): boolean {
return true;
}
get dehydratedDeviceId(): string | undefined {
return this._dehydratedDeviceId;
}
get isBusy(): boolean {
return this._isBusy;
}
get backupVersion(): string {
return this._keyBackup?.version ?? "";
}
get isMasterKeyTrusted(): boolean {
return this._crossSigning?.isMasterKeyTrusted ?? false;
}
get canSignOwnDevice(): boolean {
return !!this._crossSigning;
}
async signOwnDevice(): Promise<void> {
const crossSigning = this._crossSigning;
if (crossSigning) {
await this.logger.run("KeyBackupViewModel.signOwnDevice", async log => {
await crossSigning.signOwnDevice(log);
});
}
}
get backupWriteStatus(): BackupWriteStatus {
const keyBackup = this._keyBackup;
if (!keyBackup || keyBackup.version === undefined) {
return BackupWriteStatus.Pending;
} else if (keyBackup.hasStopped) {
return BackupWriteStatus.Stopped;
}
const operation = keyBackup.operationInProgress;
if (operation) {
return BackupWriteStatus.Writing;
} else if (keyBackup.hasBackedUpAllKeys) {
return BackupWriteStatus.Done;
} else {
return BackupWriteStatus.Pending;
}
}
get backupError(): string | undefined {
return this._keyBackup?.error?.message;
}
get error(): string | undefined {
return this._error?.message;
}
showPhraseSetup(): void {
if (this._status === Status.Setup) {
this._setupKeyType = KeyType.Passphrase;
this.emitChange("setupKeyType");
}
}
showKeySetup(): void {
if (this._status === Status.Setup) {
this._setupKeyType = KeyType.Passphrase;
this.emitChange("setupKeyType");
}
}
get setupKeyType(): KeyType {
return this._setupKeyType;
}
private async _enterCredentials(keyType, credential, setupDehydratedDevice): Promise<void> {
if (credential) {
try {
this._isBusy = true;
this.emitChange("isBusy");
const key = await this._session.enableSecretStorage(keyType, credential);
if (setupDehydratedDevice) {
this._dehydratedDeviceId = await this._session.setupDehydratedDevice(key);
}
} catch (err) {
console.error(err);
this._error = err;
this.emitChange("error");
} finally {
this._isBusy = false;
this.emitChange();
}
}
}
enterSecurityPhrase(passphrase, setupDehydratedDevice): Promise<void> {
return this._enterCredentials(KeyType.Passphrase, passphrase, setupDehydratedDevice);
}
enterSecurityKey(securityKey, setupDehydratedDevice): Promise<void> {
return this._enterCredentials(KeyType.RecoveryKey, securityKey, setupDehydratedDevice);
}
async disable(): Promise<void> {
try {
this._isBusy = true;
this.emitChange("isBusy");
await this._session.disableSecretStorage();
} catch (err) {
console.error(err);
this._error = err;
this.emitChange("error");
} finally {
this._isBusy = false;
this.emitChange();
}
}
get isBackingUp(): boolean {
return this._keyBackup?.operationInProgress !== undefined;
}
get backupPercentage(): number {
if (this._progress) {
return Math.round((this._progress.finished / this._progress.total) * 100);
}
return 0;
}
get backupInProgressLabel(): string {
if (this._progress) {
return this.i18n`${this._progress.finished} of ${this._progress.total}`;
}
return this.i18n``;
}
cancelBackup(): void {
this._keyBackup?.operationInProgress?.abort();
}
startBackup(): void {
this.logger.run("KeyBackupViewModel.startBackup", log => {
this._keyBackup?.flush(log);
});
}
}

View File

@ -15,7 +15,7 @@ limitations under the License.
*/
import {ViewModel} from "../../ViewModel";
import {KeyBackupViewModel} from "./KeyBackupViewModel.js";
import {KeyBackupViewModel} from "./KeyBackupViewModel";
import {FeaturesViewModel} from "./FeaturesViewModel";
import {submitLogsFromSessionToDefaultServer} from "../../../domain/rageshake";

View File

@ -90,7 +90,7 @@ export class Session {
this._getSyncToken = () => this.syncToken;
this._olmWorker = olmWorker;
this._keyBackup = new ObservableValue(undefined);
this._crossSigning = undefined;
this._crossSigning = new ObservableValue(undefined);
this._observedRoomStatus = new Map();
if (olm) {
@ -250,7 +250,7 @@ export class Session {
}
if (this._keyBackup.get()) {
this._keyBackup.get().dispose();
this._keyBackup.set(null);
this._keyBackup.set(undefined);
}
// TODO: stop cross-signing
const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm);
@ -258,8 +258,8 @@ export class Session {
// only after having read a secret, write the key
// as we only find out if it was good if the MAC verification succeeds
await this._writeSSSSKey(key, log);
await this._keyBackup?.start(log);
await this._crossSigning?.start(log);
await this._keyBackup.get()?.start(log);
await this._crossSigning.get()?.start(log);
return key;
} else {
throw new Error("Could not read key backup with the given key");
@ -331,29 +331,21 @@ export class Session {
if (isValid) {
await this._loadSecretStorageServices(secretStorage, txn, log);
}
if (!this._keyBackup.get()) {
// null means key backup isn't configured yet
// as opposed to undefined, which means we're still checking
this._keyBackup.set(null);
}
return isValid;
});
}
_loadSecretStorageServices(secretStorage, txn, log) {
async _loadSecretStorageServices(secretStorage, txn, log) {
try {
await log.wrap("enable key backup", async log => {
// TODO: delay network request here until start()
const keyBackup = await KeyBackup.fromSecretStorage(
this._platform,
this._olm,
secretStorage,
const keyBackup = new KeyBackup(
this._hsApi,
this._olm,
this._keyLoader,
this._storage,
txn
this._platform,
);
if (keyBackup) {
if (await keyBackup.load(secretStorage, txn)) {
for (const room of this._rooms.values()) {
if (room.isEncrypted) {
room.enableKeyBackup(keyBackup);
@ -378,8 +370,8 @@ export class Session {
ownUserId: this.userId,
e2eeAccount: this._e2eeAccount
});
if (crossSigning.load(txn, log)) {
this._crossSigning = crossSigning;
if (await crossSigning.load(txn, log)) {
this._crossSigning.set(crossSigning);
}
});
}
@ -585,8 +577,8 @@ export class Session {
}
});
}
this._keyBackup?.start(log);
this._crossSigning?.start(log);
this._keyBackup.get()?.start(log);
this._crossSigning.get()?.start(log);
// restore unfinished operations, like sending out room keys
const opsTxn = await this._storage.readWriteTxn([

View File

@ -20,6 +20,8 @@ import {MEGOLM_ALGORITHM} from "../../common";
import * as Curve25519 from "./Curve25519";
import {AbortableOperation} from "../../../../utils/AbortableOperation";
import {ObservableValue} from "../../../../observable/value";
import {Deferred} from "../../../../utils/Deferred";
import {EventEmitter} from "../../../../utils/EventEmitter";
import {SetAbortableFn} from "../../../../utils/AbortableOperation";
import type {BackupInfo, SessionData, SessionKeyInfo, SessionInfo, KeyBackupPayload} from "./types";
@ -31,43 +33,69 @@ 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";
import type {IHomeServerRequest} from "../../../net/HomeServerRequest";
import type * as OlmNamespace from "@matrix-org/olm";
type Olm = typeof OlmNamespace;
const KEYS_PER_REQUEST = 200;
export class KeyBackup {
public readonly operationInProgress = new ObservableValue<AbortableOperation<Promise<void>, Progress> | undefined>(undefined);
// a set of fields we need to store once we've fetched
// the backup info from the homeserver, which happens in start()
class BackupConfig {
constructor(
public readonly info: BackupInfo,
public readonly crypto: Curve25519.BackupEncryption
) {}
}
export class KeyBackup extends EventEmitter<{change: never}> {
private _operationInProgress?: AbortableOperation<Promise<void>, Progress>;
private _stopped = false;
private _needsNewKey = false;
private _hasBackedUpAllKeys = false;
private _error?: Error;
private crypto?: Curve25519.BackupEncryption;
private backupInfo?: BackupInfo;
private privateKey?: Uint8Array;
private backupConfigDeferred: Deferred<BackupConfig | undefined> = new Deferred();
private backupInfoRequest?: IHomeServerRequest;
constructor(
private readonly backupInfo: BackupInfo,
private readonly hsApi: HomeServerApi,
private readonly olm: Olm,
private readonly keyLoader: KeyLoader,
private readonly storage: Storage,
private readonly platform: Platform,
private readonly maxDelay: number = 10000
) {}
) {
super();
// doing the network request for getting the backup info
// and hence creating the crypto instance depending on the chose algorithm
// is delayed until start() is called, but we want to already take requests
// for fetching the room keys, so put the crypto and backupInfo in a deferred.
this.backupConfigDeferred = new Deferred();
}
get hasStopped(): boolean { return this._stopped; }
get error(): Error | undefined { return this._error; }
get version(): string { return this.backupInfo.version; }
get version(): string | undefined { return this.backupConfigDeferred.value?.info?.version; }
get needsNewKey(): boolean { return this._needsNewKey; }
get hasBackedUpAllKeys(): boolean { return this._hasBackedUpAllKeys; }
get operationInProgress(): AbortableOperation<Promise<void>, Progress> | undefined { return this._operationInProgress; }
async getRoomKey(roomId: string, sessionId: string, log: ILogItem): Promise<IncomingRoomKey | undefined> {
if (this.needsNewKey || !this.crypto) {
if (this.needsNewKey) {
return;
}
const sessionResponse = await this.hsApi.roomKeyForRoomAndSession(this.backupInfo.version, roomId, sessionId, {log}).response();
const backupConfig = await this.backupConfigDeferred.promise;
if (!backupConfig) {
return;
}
const sessionResponse = await this.hsApi.roomKeyForRoomAndSession(backupConfig.info.version, roomId, sessionId, {log}).response();
if (!sessionResponse.session_data) {
return;
}
const sessionKeyInfo = this.crypto.decryptRoomKey(sessionResponse.session_data as SessionData);
const sessionKeyInfo = backupConfig.crypto.decryptRoomKey(sessionResponse.session_data as SessionData);
if (sessionKeyInfo?.algorithm === MEGOLM_ALGORITHM) {
return keyFromBackup(roomId, sessionId, sessionKeyInfo);
} else if (sessionKeyInfo?.algorithm) {
@ -79,14 +107,53 @@ export class KeyBackup {
return txn.inboundGroupSessions.markAllAsNotBackedUp();
}
start(log: ILogItem) {
async load(secretStorage: SecretStorage, txn: Transaction) {
// TODO: no exception here we should anticipate?
const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1", txn);
if (base64PrivateKey) {
this.privateKey = new Uint8Array(this.platform.encoding.base64.decode(base64PrivateKey));
return true;
} else {
this.backupConfigDeferred.resolve(undefined);
return false;
}
}
// fetch latest version
this.flush(log);
async start(log: ILogItem) {
await log.wrap("KeyBackup.start", async log => {
if (this.privateKey && !this.backupInfoRequest) {
let backupInfo: BackupInfo;
try {
this.backupInfoRequest = this.hsApi.roomKeysVersion(undefined, {log});
backupInfo = await this.backupInfoRequest.response() as BackupInfo;
} catch (err) {
if (err.name === "AbortError") {
log.set("aborted", true);
return;
} else {
throw err;
}
} finally {
this.backupInfoRequest = undefined;
}
// TODO: what if backupInfo is undefined or we get 404 or something?
if (backupInfo.algorithm === Curve25519.Algorithm) {
const crypto = Curve25519.BackupEncryption.fromAuthData(backupInfo.auth_data, this.privateKey, this.olm);
this.backupConfigDeferred.resolve(new BackupConfig(backupInfo, crypto));
this.emit("change");
} else {
this.backupConfigDeferred.resolve(undefined);
log.log({l: `Unknown backup algorithm`, algorithm: backupInfo.algorithm});
}
this.privateKey = undefined;
}
// fetch latest version
this.flush(log);
});
}
flush(log: ILogItem): void {
if (!this.operationInProgress.get()) {
if (!this._operationInProgress) {
log.wrapDetached("flush key backup", async log => {
if (this._needsNewKey) {
log.set("needsNewKey", this._needsNewKey);
@ -96,7 +163,8 @@ export class KeyBackup {
this._error = undefined;
this._hasBackedUpAllKeys = false;
const operation = this._runFlushOperation(log);
this.operationInProgress.set(operation);
this._operationInProgress = operation;
this.emit("change");
try {
await operation.result;
this._hasBackedUpAllKeys = true;
@ -113,13 +181,18 @@ export class KeyBackup {
}
log.catch(err);
}
this.operationInProgress.set(undefined);
this._operationInProgress = undefined;
this.emit("change");
});
}
}
private _runFlushOperation(log: ILogItem): AbortableOperation<Promise<void>, Progress> {
return new AbortableOperation(async (setAbortable, setProgress) => {
const backupConfig = await this.backupConfigDeferred.promise;
if (!backupConfig) {
return;
}
let total = 0;
let amountFinished = 0;
while (true) {
@ -138,8 +211,8 @@ export class KeyBackup {
log.set("total", total);
return;
}
const payload = await this.encodeKeysForBackup(keysNeedingBackup);
const uploadRequest = this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload, {log});
const payload = await this.encodeKeysForBackup(keysNeedingBackup, backupConfig.crypto);
const uploadRequest = this.hsApi.uploadRoomKeysToBackup(backupConfig.info.version, payload, {log});
setAbortable(uploadRequest);
await uploadRequest.response();
await this.markKeysAsBackedUp(keysNeedingBackup, setAbortable);
@ -149,7 +222,7 @@ export class KeyBackup {
});
}
private async encodeKeysForBackup(roomKeys: RoomKey[]): Promise<KeyBackupPayload> {
private async encodeKeysForBackup(roomKeys: RoomKey[], crypto: Curve25519.BackupEncryption): Promise<KeyBackupPayload> {
const payload: KeyBackupPayload = { rooms: {} };
const payloadRooms = payload.rooms;
for (const key of roomKeys) {
@ -157,7 +230,7 @@ export class KeyBackup {
if (!roomPayload) {
roomPayload = payloadRooms[key.roomId] = { sessions: {} };
}
roomPayload.sessions[key.sessionId] = await this.encodeRoomKey(key);
roomPayload.sessions[key.sessionId] = await this.encodeRoomKey(key, crypto);
}
return payload;
}
@ -178,7 +251,7 @@ export class KeyBackup {
await txn.complete();
}
private async encodeRoomKey(roomKey: RoomKey): Promise<SessionInfo> {
private async encodeRoomKey(roomKey: RoomKey, crypto: Curve25519.BackupEncryption): Promise<SessionInfo> {
return await this.keyLoader.useKey(roomKey, session => {
const firstMessageIndex = session.first_known_index();
const sessionKey = session.export_session(firstMessageIndex);
@ -186,27 +259,14 @@ export class KeyBackup {
first_message_index: firstMessageIndex,
forwarded_count: 0,
is_verified: false,
session_data: this.crypto.encryptRoomKey(roomKey, sessionKey)
session_data: crypto.encryptRoomKey(roomKey, sessionKey)
};
});
}
dispose() {
this.crypto?.dispose();
}
static async fromSecretStorage(platform: Platform, olm: Olm, secretStorage: SecretStorage, hsApi: HomeServerApi, keyLoader: KeyLoader, storage: Storage, txn: Transaction): Promise<KeyBackup | undefined> {
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;
if (backupInfo.algorithm === Curve25519.Algorithm) {
const crypto = Curve25519.BackupEncryption.fromAuthData(backupInfo.auth_data, privateKey, olm);
return new KeyBackup(backupInfo, privateKey, hsApi, keyLoader, storage, platform);
} else {
throw new Error(`Unknown backup algorithm: ${backupInfo.algorithm}`);
}
}
this.backupInfoRequest?.abort();
this.backupConfigDeferred.value?.crypto?.dispose();
}
}

View File

@ -50,10 +50,20 @@ export class SecretStorage {
const allAccountData = await txn.accountData.getAll();
for (const accountData of allAccountData) {
try {
// TODO: fix this, using the webcrypto api closes the transaction
if (accountData.type === "m.megolm_backup.v1") {
return true;
} else {
continue;
}
const secret = await this._decryptAccountData(accountData);
return true; // decryption succeeded
} catch (err) {
continue;
if (err instanceof DecryptionError && err.reason !== DecryptionFailure.NotEncryptedWithKey) {
throw err;
} else {
continue;
}
}
}
return false;

View File

@ -15,7 +15,7 @@ limitations under the License.
*/
import {TemplateView} from "../general/TemplateView";
import {KeyBackupSettingsView} from "../session/settings/KeyBackupSettingsView.js";
import {KeyBackupSettingsView} from "../session/settings/KeyBackupSettingsView";
export class AccountSetupView extends TemplateView {
render(t, vm) {

View File

@ -14,32 +14,41 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {TemplateView} from "../../general/TemplateView";
import {TemplateView, Builder} from "../../general/TemplateView";
import {disableTargetCallback} from "../../general/utils";
import {ViewNode} from "../../general/types";
import {KeyBackupViewModel, Status, BackupWriteStatus} from "../../../../../domain/session/settings/KeyBackupViewModel";
import {KeyType} from "../../../../../matrix/ssss/index";
export class KeyBackupSettingsView extends TemplateView {
render(t, vm) {
export class KeyBackupSettingsView extends TemplateView<KeyBackupViewModel> {
render(t: Builder<KeyBackupViewModel>, vm: KeyBackupViewModel): ViewNode {
return t.div([
t.map(vm => vm.status, (status, t, vm) => {
switch (status) {
case "Enabled": return renderEnabled(t, vm);
case "NewVersionAvailable": return renderNewVersionAvailable(t, vm);
case "SetupKey": return renderEnableFromKey(t, vm);
case "SetupPhrase": return renderEnableFromPhrase(t, vm);
case "Pending": return t.p(vm.i18n`Waiting to go online…`);
case Status.Enabled: return renderEnabled(t, vm);
case Status.NewVersionAvailable: return renderNewVersionAvailable(t, vm);
case Status.Setup: {
if (vm.setupKeyType === KeyType.Passphrase) {
return renderEnableFromPhrase(t, vm);
} else {
return renderEnableFromKey(t, vm);
}
break;
}
case Status.Pending: return t.p(vm.i18n`Waiting to go online…`);
}
}),
t.map(vm => vm.backupWriteStatus, (status, t, vm) => {
switch (status) {
case "Writing": {
case BackupWriteStatus.Writing: {
const progress = t.progress({
min: 0,
max: 100,
min: 0+"",
max: 100+"",
value: vm => vm.backupPercentage,
});
return t.div([`Backup in progress `, progress, " ", vm => vm.backupInProgressLabel]);
}
case "Stopped": {
case BackupWriteStatus.Stopped: {
let label;
const error = vm.backupError;
if (error) {
@ -47,12 +56,12 @@ export class KeyBackupSettingsView extends TemplateView {
} else {
label = `Backup has stopped`;
}
return t.p(label, " ", t.button({onClick: () => vm.startBackup()}, `Backup now`));
return t.p([label, " ", t.button({onClick: () => vm.startBackup()}, `Backup now`)]);
}
case "Done":
case BackupWriteStatus.Done:
return t.p(`All keys are backed up.`);
default:
return null;
return undefined;
}
}),
t.if(vm => vm.isMasterKeyTrusted, t => {
@ -70,7 +79,7 @@ export class KeyBackupSettingsView extends TemplateView {
}
}
function renderEnabled(t, vm) {
function renderEnabled(t: Builder<KeyBackupViewModel>, vm: KeyBackupViewModel): ViewNode {
const items = [
t.p([vm.i18n`Key backup is enabled, using backup version ${vm.backupVersion}. `, t.button({onClick: () => vm.disable()}, vm.i18n`Disable`)])
];
@ -80,14 +89,14 @@ function renderEnabled(t, vm) {
return t.div(items);
}
function renderNewVersionAvailable(t, vm) {
function renderNewVersionAvailable(t: Builder<KeyBackupViewModel>, vm: KeyBackupViewModel): ViewNode {
const items = [
t.p([vm.i18n`A new backup version has been created from another device. Disable key backup and enable it again with the new key.`, t.button({onClick: () => vm.disable()}, vm.i18n`Disable`)])
];
return t.div(items);
}
function renderEnableFromKey(t, vm) {
function renderEnableFromKey(t: Builder<KeyBackupViewModel>, vm: KeyBackupViewModel): ViewNode {
const useASecurityPhrase = t.button({className: "link", onClick: () => vm.showPhraseSetup()}, vm.i18n`use a security phrase`);
return t.div([
t.p(vm.i18n`Enter your secret storage security key below to ${vm.purpose}, which will enable you to decrypt messages received before you logged into this session. The security key is a code of 12 groups of 4 characters separated by a space that Element created for you when setting up security.`),
@ -97,7 +106,7 @@ function renderEnableFromKey(t, vm) {
]);
}
function renderEnableFromPhrase(t, vm) {
function renderEnableFromPhrase(t: Builder<KeyBackupViewModel>, vm: KeyBackupViewModel): ViewNode {
const useASecurityKey = t.button({className: "link", onClick: () => vm.showKeySetup()}, vm.i18n`use your security key`);
return t.div([
t.p(vm.i18n`Enter your secret storage security phrase below to ${vm.purpose}, which will enable you to decrypt messages received before you logged into this session. The security phrase is a freeform secret phrase you optionally chose when setting up security in Element. It is different from your password to login, unless you chose to set them to the same value.`),
@ -107,7 +116,7 @@ function renderEnableFromPhrase(t, vm) {
]);
}
function renderEnableFieldRow(t, vm, label, callback) {
function renderEnableFieldRow(t, vm, label, callback): ViewNode {
let setupDehydrationCheck;
const eventHandler = () => callback(input.value, setupDehydrationCheck?.checked || false);
const input = t.input({type: "password", disabled: vm => vm.isBusy, placeholder: label});
@ -131,8 +140,8 @@ function renderEnableFieldRow(t, vm, label, callback) {
]);
}
function renderError(t) {
return t.if(vm => vm.error, (t, vm) => {
function renderError(t: Builder<KeyBackupViewModel>): ViewNode {
return t.if(vm => vm.error !== undefined, (t, vm) => {
return t.div([
t.p({className: "error"}, vm => vm.i18n`Could not enable key backup: ${vm.error}.`),
t.p(vm.i18n`Try double checking that you did not mix up your security key, security phrase and login password as explained above.`)

View File

@ -16,7 +16,7 @@ limitations under the License.
import {TemplateView} from "../../general/TemplateView";
import {disableTargetCallback} from "../../general/utils";
import {KeyBackupSettingsView} from "./KeyBackupSettingsView.js"
import {KeyBackupSettingsView} from "./KeyBackupSettingsView"
import {FeaturesView} from "./FeaturesView"
export class SettingsView extends TemplateView {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableValue, ObservableValue} from "../observable/value";
import {EventEmitter} from "../utils/EventEmitter";
export interface IAbortable {
abort();
@ -24,25 +24,27 @@ export type SetAbortableFn = (a: IAbortable) => typeof a;
export type SetProgressFn<P> = (progress: P) => void;
type RunFn<T, P> = (setAbortable: SetAbortableFn, setProgress: SetProgressFn<P>) => T;
export class AbortableOperation<T, P = void> implements IAbortable {
export class AbortableOperation<T, P = void> extends EventEmitter<{change: keyof AbortableOperation<T, P>}> implements IAbortable {
public readonly result: T;
private _abortable?: IAbortable;
private _progress: ObservableValue<P | undefined>;
private _progress?: P;
constructor(run: RunFn<T, P>) {
super();
this._abortable = undefined;
const setAbortable: SetAbortableFn = abortable => {
this._abortable = abortable;
return abortable;
};
this._progress = new ObservableValue<P | undefined>(undefined);
this._progress = undefined;
const setProgress: SetProgressFn<P> = (progress: P) => {
this._progress.set(progress);
this._progress = progress;
this.emit("change", "progress");
};
this.result = run(setAbortable, setProgress);
}
get progress(): BaseObservableValue<P | undefined> {
get progress(): P | undefined {
return this._progress;
}

41
src/utils/Deferred.ts Normal file
View File

@ -0,0 +1,41 @@
/*
Copyright 2023 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.
*/
export class Deferred<T> {
public readonly promise: Promise<T>;
public readonly resolve: (value: T) => void;
public readonly reject: (err: Error) => void;
private _value?: T;
constructor() {
let resolve;
let reject;
this.promise = new Promise<T>((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
})
this.resolve = (value: T) => {
this._value = value;
resolve(value);
};
this.reject = reject;
}
get value(): T | undefined {
return this._value;
}
}