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 {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() {}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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`),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -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`.`]),
|
||||
|
|
Reference in a new issue