use same UI as in settings to pick between recovery key and passphrase
This commit is contained in:
parent
44a26fd340
commit
6d9d8797fe
5 changed files with 115 additions and 23 deletions
|
@ -16,16 +16,25 @@ limitations under the License.
|
||||||
|
|
||||||
import {ViewModel} from "./ViewModel.js";
|
import {ViewModel} from "./ViewModel.js";
|
||||||
import {KeyType} from "../matrix/ssss/index.js";
|
import {KeyType} from "../matrix/ssss/index.js";
|
||||||
|
import {Status} from "./session/settings/SessionBackupViewModel.js";
|
||||||
|
|
||||||
export class AccountSetupViewModel extends ViewModel {
|
export class AccountSetupViewModel extends ViewModel {
|
||||||
constructor(accountSetup) {
|
constructor(accountSetup) {
|
||||||
super();
|
super();
|
||||||
this._accountSetup = accountSetup;
|
this._accountSetup = accountSetup;
|
||||||
this._dehydratedDevice = undefined;
|
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() {
|
get decryptDehydratedDeviceViewModel() {
|
||||||
return !!this._accountSetup.encryptedDehydratedDevice;
|
return this._decryptDehydratedDeviceViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
get deviceDecrypted() {
|
get deviceDecrypted() {
|
||||||
|
@ -36,15 +45,92 @@ export class AccountSetupViewModel extends ViewModel {
|
||||||
return this._accountSetup.encryptedDehydratedDevice.deviceId;
|
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() {
|
finish() {
|
||||||
this._accountSetup.finish(this._dehydratedDevice);
|
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() {}
|
||||||
|
}
|
||||||
|
|
|
@ -125,7 +125,7 @@ export class SessionLoadViewModel extends ViewModel {
|
||||||
case LoadStatus.QueryAccount:
|
case LoadStatus.QueryAccount:
|
||||||
return `Querying account encryption setup…`;
|
return `Querying account encryption setup…`;
|
||||||
case LoadStatus.AccountSetup:
|
case LoadStatus.AccountSetup:
|
||||||
return `Do you want to restore this dehydrated device?`;
|
return ""; // we'll show a header ing AccountSetupView
|
||||||
case LoadStatus.SessionSetup:
|
case LoadStatus.SessionSetup:
|
||||||
return `Setting up your encryption keys…`;
|
return `Setting up your encryption keys…`;
|
||||||
case LoadStatus.Loading:
|
case LoadStatus.Loading:
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {ViewModel} from "../../ViewModel.js";
|
||||||
import {KeyType} from "../../../matrix/ssss/index.js";
|
import {KeyType} from "../../../matrix/ssss/index.js";
|
||||||
import {createEnum} from "../../../utils/enum.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 {
|
export class SessionBackupViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
|
@ -58,6 +58,10 @@ export class SessionBackupViewModel extends ViewModel {
|
||||||
return this.i18n`Set up`;
|
return this.i18n`Set up`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get purpose() {
|
||||||
|
return this.i18n`set up session backup`;
|
||||||
|
}
|
||||||
|
|
||||||
offerDehydratedDeviceSetup() {
|
offerDehydratedDeviceSetup() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {TemplateView} from "../general/TemplateView";
|
import {TemplateView} from "../general/TemplateView";
|
||||||
|
import {SessionBackupSettingsView} from "../session/settings/SessionBackupSettingsView.js";
|
||||||
|
|
||||||
export class AccountSetupView extends TemplateView {
|
export class AccountSetupView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
|
@ -25,21 +26,22 @@ export class AccountSetupView extends TemplateView {
|
||||||
placeholder: keyLabel,
|
placeholder: keyLabel,
|
||||||
});
|
});
|
||||||
const status = t.output({for: password.id}, vm => vm.deviceDecrypted ? "Key matches, good to go!" : "");
|
const status = t.output({for: password.id}, vm => vm.deviceDecrypted ? "Key matches, good to go!" : "");
|
||||||
return t.div({className: "form"}, [
|
return t.div({className: "Settings" /* hack for now to get the layout right*/}, [
|
||||||
t.form({
|
t.h3(vm.i18n`Restore your encrypted history?`),
|
||||||
onSubmit: evt => {
|
t.ifView(vm => vm.decryptDehydratedDeviceViewModel, vm => new SessionBackupSettingsView(vm.decryptDehydratedDeviceViewModel)),
|
||||||
evt.preventDefault();
|
t.map(vm => vm.deviceDecrypted, (decrypted, t) => {
|
||||||
vm.tryDecryptDehydratedDevice(password.value);
|
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.div({ className: "button-row" }, [
|
||||||
t.button({
|
t.button({
|
||||||
className: "button-action primary",
|
className: "button-action primary",
|
||||||
onClick: () => { vm.finish(); },
|
onClick: () => { vm.finish(); },
|
||||||
type: "button",
|
type: "button",
|
||||||
}, vm.i18n`Continue`),
|
}, vm => vm.deviceDecrypted ? vm.i18n`Continue` : vm.i18n`Continue without restoring`),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ function renderEnabled(t, vm) {
|
||||||
function renderEnableFromKey(t, vm) {
|
function renderEnableFromKey(t, vm) {
|
||||||
const useASecurityPhrase = t.button({className: "link", onClick: () => vm.showPhraseSetup()}, vm.i18n`use a security phrase`);
|
const useASecurityPhrase = t.button({className: "link", onClick: () => vm.showPhraseSetup()}, vm.i18n`use a security phrase`);
|
||||||
return t.div([
|
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),
|
renderError(t),
|
||||||
renderEnableFieldRow(t, vm, vm.i18n`Security key`, (key, setupDehydratedDevice) => vm.enterSecurityKey(key, setupDehydratedDevice)),
|
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.`]),
|
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) {
|
function renderEnableFromPhrase(t, vm) {
|
||||||
const useASecurityKey = t.button({className: "link", onClick: () => vm.showKeySetup()}, vm.i18n`use your security key`);
|
const useASecurityKey = t.button({className: "link", onClick: () => vm.showKeySetup()}, vm.i18n`use your security key`);
|
||||||
return t.div([
|
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),
|
renderError(t),
|
||||||
renderEnableFieldRow(t, vm, vm.i18n`Security phrase`, (phrase, setupDehydratedDevice) => vm.enterSecurityPhrase(phrase, setupDehydratedDevice)),
|
renderEnableFieldRow(t, vm, vm.i18n`Security phrase`, (phrase, setupDehydratedDevice) => vm.enterSecurityPhrase(phrase, setupDehydratedDevice)),
|
||||||
t.p([vm.i18n`You can also `, useASecurityKey, vm.i18n`.`]),
|
t.p([vm.i18n`You can also `, useASecurityKey, vm.i18n`.`]),
|
||||||
|
|
Reference in a new issue