diff --git a/src/domain/AccountSetupViewModel.js b/src/domain/AccountSetupViewModel.js index c1f9d41b..7930b87d 100644 --- a/src/domain/AccountSetupViewModel.js +++ b/src/domain/AccountSetupViewModel.js @@ -16,16 +16,25 @@ limitations under the License. import {ViewModel} from "./ViewModel.js"; import {KeyType} from "../matrix/ssss/index.js"; +import {Status} from "./session/settings/SessionBackupViewModel.js"; export class AccountSetupViewModel extends ViewModel { constructor(accountSetup) { super(); this._accountSetup = accountSetup; this._dehydratedDevice = undefined; + this._decryptDehydratedDeviceViewModel = undefined; + if (this._accountSetup.encryptedDehydratedDevice) { + this._decryptDehydratedDeviceViewModel = new DecryptDehydratedDeviceViewModel(this, dehydratedDevice => { + this._dehydratedDevice = dehydratedDevice; + this._decryptDehydratedDeviceViewModel = undefined; + this.emitChange("deviceDecrypted"); + }); + } } - get canDehydrateDevice() { - return !!this._accountSetup.encryptedDehydratedDevice; + get decryptDehydratedDeviceViewModel() { + return this._decryptDehydratedDeviceViewModel; } get deviceDecrypted() { @@ -36,15 +45,92 @@ export class AccountSetupViewModel extends ViewModel { return this._accountSetup.encryptedDehydratedDevice.deviceId; } - async tryDecryptDehydratedDevice(password) { - const {encryptedDehydratedDevice} = this._accountSetup; - if (encryptedDehydratedDevice) { - this._dehydratedDevice = await encryptedDehydratedDevice.decrypt(KeyType.RecoveryKey, password); - this.emitChange("deviceDecrypted"); - } - } - finish() { this._accountSetup.finish(this._dehydratedDevice); } } + +// this vm adopts the same shape as SessionBackupViewModel so the same view can be reused. +class DecryptDehydratedDeviceViewModel extends ViewModel { + constructor(accountSetupViewModel, decryptedCallback) { + super(); + this._accountSetupViewModel = accountSetupViewModel; + this._isBusy = false; + this._status = Status.SetupKey; + this._error = undefined; + this._decryptedCallback = decryptedCallback; + } + + get decryptAction() { + return this.i18n`Restore`; + } + + get purpose() { + return this.i18n`claim your dehydrated device`; + } + + get offerDehydratedDeviceSetup() { + return false; + } + + get dehydratedDeviceId() { + return this._accountSetupViewModel._dehydratedDevice?.deviceId; + } + + get isBusy() { + return this._isBusy; + } + + get backupVersion() { return 0; } + + 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) { + if (credential) { + try { + this._isBusy = true; + this.emitChange("isBusy"); + const {encryptedDehydratedDevice} = this._accountSetupViewModel._accountSetup; + const dehydratedDevice = await encryptedDehydratedDevice.decrypt(keyType, credential); + this._decryptedCallback(dehydratedDevice); + } catch (err) { + console.error(err); + this._error = err; + this.emitChange("error"); + } finally { + this._isBusy = false; + this.emitChange(""); + } + } + } + + enterSecurityPhrase(passphrase) { + this._enterCredentials(KeyType.Passphrase, passphrase); + } + + enterSecurityKey(securityKey) { + this._enterCredentials(KeyType.RecoveryKey, securityKey); + } + + disable() {} +} diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js index eec7c2fb..c798bc67 100644 --- a/src/domain/SessionLoadViewModel.js +++ b/src/domain/SessionLoadViewModel.js @@ -125,7 +125,7 @@ export class SessionLoadViewModel extends ViewModel { case LoadStatus.QueryAccount: return `Querying account encryption setup…`; case LoadStatus.AccountSetup: - return `Do you want to restore this dehydrated device?`; + return ""; // we'll show a header ing AccountSetupView case LoadStatus.SessionSetup: return `Setting up your encryption keys…`; case LoadStatus.Loading: diff --git a/src/domain/session/settings/SessionBackupViewModel.js b/src/domain/session/settings/SessionBackupViewModel.js index 014fc99f..533d0f6d 100644 --- a/src/domain/session/settings/SessionBackupViewModel.js +++ b/src/domain/session/settings/SessionBackupViewModel.js @@ -18,7 +18,7 @@ import {ViewModel} from "../../ViewModel.js"; import {KeyType} from "../../../matrix/ssss/index.js"; import {createEnum} from "../../../utils/enum.js"; -const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending"); +export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending"); export class SessionBackupViewModel extends ViewModel { constructor(options) { @@ -58,6 +58,10 @@ export class SessionBackupViewModel extends ViewModel { return this.i18n`Set up`; } + get purpose() { + return this.i18n`set up session backup`; + } + offerDehydratedDeviceSetup() { return true; } diff --git a/src/platform/web/ui/login/AccountSetupView.js b/src/platform/web/ui/login/AccountSetupView.js index 2438a6df..1855e82f 100644 --- a/src/platform/web/ui/login/AccountSetupView.js +++ b/src/platform/web/ui/login/AccountSetupView.js @@ -15,6 +15,7 @@ limitations under the License. */ import {TemplateView} from "../general/TemplateView"; +import {SessionBackupSettingsView} from "../session/settings/SessionBackupSettingsView.js"; export class AccountSetupView extends TemplateView { render(t, vm) { @@ -25,21 +26,22 @@ export class AccountSetupView extends TemplateView { placeholder: keyLabel, }); const status = t.output({for: password.id}, vm => vm.deviceDecrypted ? "Key matches, good to go!" : ""); - return t.div({className: "form"}, [ - t.form({ - onSubmit: evt => { - evt.preventDefault(); - vm.tryDecryptDehydratedDevice(password.value); + return t.div({className: "Settings" /* hack for now to get the layout right*/}, [ + t.h3(vm.i18n`Restore your encrypted history?`), + t.ifView(vm => vm.decryptDehydratedDeviceViewModel, vm => new SessionBackupSettingsView(vm.decryptDehydratedDeviceViewModel)), + t.map(vm => vm.deviceDecrypted, (decrypted, t) => { + if (decrypted) { + return t.p(vm.i18n`That worked out, you're good to go!`); + } else { + return t.p(vm.i18n`This will claim the dehydrated device ${vm.dehydratedDeviceId}, and will set up a new one.`); } - }, [ - t.div({ className: "form-row" }, [t.label({ for: password.id }, keyLabel), password, status]), - ]), + }), t.div({ className: "button-row" }, [ t.button({ className: "button-action primary", onClick: () => { vm.finish(); }, type: "button", - }, vm.i18n`Continue`), + }, vm => vm.deviceDecrypted ? vm.i18n`Continue` : vm.i18n`Continue without restoring`), ]), ]); } diff --git a/src/platform/web/ui/session/settings/SessionBackupSettingsView.js b/src/platform/web/ui/session/settings/SessionBackupSettingsView.js index 25d8b944..e989f8ea 100644 --- a/src/platform/web/ui/session/settings/SessionBackupSettingsView.js +++ b/src/platform/web/ui/session/settings/SessionBackupSettingsView.js @@ -43,7 +43,7 @@ function renderEnabled(t, vm) { function renderEnableFromKey(t, vm) { 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 set up session backup, 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.`), + 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.`), renderError(t), renderEnableFieldRow(t, vm, vm.i18n`Security key`, (key, setupDehydratedDevice) => vm.enterSecurityKey(key, setupDehydratedDevice)), t.p([vm.i18n`Alternatively, you can `, useASecurityPhrase, vm.i18n` if you have one.`]), @@ -53,7 +53,7 @@ function renderEnableFromKey(t, vm) { function renderEnableFromPhrase(t, vm) { 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 set up session backup, 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.`), + 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.`), renderError(t), renderEnableFieldRow(t, vm, vm.i18n`Security phrase`, (phrase, setupDehydratedDevice) => vm.enterSecurityPhrase(phrase, setupDehydratedDevice)), t.p([vm.i18n`You can also `, useASecurityKey, vm.i18n`.`]),