use same UI as in settings to pick between recovery key and passphrase

This commit is contained in:
Bruno Windels 2021-10-29 16:40:35 +02:00
parent 44a26fd340
commit 6d9d8797fe
5 changed files with 115 additions and 23 deletions

View file

@ -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() {}
}

View file

@ -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:

View file

@ -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;
}

View file

@ -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`),
]),
]);
}

View file

@ -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`.`]),