better session backup ui

This commit is contained in:
Bruno Windels 2020-10-19 18:29:13 +02:00
parent 8f89c7b363
commit 6f82d81f39
10 changed files with 200 additions and 37 deletions

View file

@ -138,26 +138,4 @@ export class SessionStatusViewModel extends ViewModel {
this._reconnector.tryNow();
}
}
async enterPassphrase(passphrase) {
if (passphrase) {
try {
await this._session.enableSecretStorage("passphrase", passphrase);
} catch (err) {
console.error(err);
alert(`Could not set up secret storage with passphrase: ${err.message}`);
}
}
}
async enterSecurityKey(securityKey) {
if (securityKey) {
try {
await this._session.enableSecretStorage("recoverykey", securityKey);
} catch (err) {
console.error(err);
alert(`Could not set up secret storage with securityKey: ${err.message}`);
}
}
}
}

View file

@ -21,8 +21,10 @@ export class SettingsViewModel extends ViewModel {
constructor(options) {
super(options);
this._updateService = options.updateService;
this._session = options.session;
this._closeUrl = this.urlCreator.urlUntilSegment("session");
const session = options.session;
this._session = session;
this._sessionBackupViewModel = this.track(new SessionBackupViewModel(this.childOptions({session})));
this._closeUrl = this.urlCreator.urlUntilSegment("session");
}
get closeUrl() {
@ -52,7 +54,7 @@ export class SettingsViewModel extends ViewModel {
if (this._updateService) {
return `${this._updateService.version} (${this._updateService.buildHash})`;
}
return "development version";
return this.i18n`development version`;
}
checkForUpdate() {
@ -62,4 +64,83 @@ export class SettingsViewModel extends ViewModel {
get showUpdateButton() {
return !!this._updateService;
}
get sessionBackupViewModel() {
return this._sessionBackupViewModel;
}
}
class SessionBackupViewModel extends ViewModel {
constructor(options) {
super(options);
this._session = options.session;
this._showKeySetup = true;
this._error = null;
this._isBusy = false;
}
get isBusy() {
return this._isBusy;
}
get backupVersion() {
return this._session.sessionBackup?.version;
}
get status() {
if (this._session.sessionBackup) {
return "enabled";
} else {
return this._showKeySetup ? "setupKey" : "setupPhrase";
}
}
get error() {
return this._error?.message;
}
showPhraseSetup() {
this._showKeySetup = false;
this.emitChange("showKeySetup");
}
showKeySetup() {
this._showKeySetup = true;
this.emitChange("showKeySetup");
}
async enterSecurityPhrase(passphrase) {
if (passphrase) {
try {
this._isBusy = true;
this.emitChange("isBusy");
await this._session.enableSecretStorage("phrase", passphrase);
} catch (err) {
console.error(err);
this._error = err;
this.emitChange("error");
} finally {
this._isBusy = false;
this.emitChange("isBusy");
}
}
}
async enterSecurityKey(securityKey) {
if (securityKey) {
try {
this._isBusy = true;
this.emitChange("isBusy");
await this._session.enableSecretStorage("key", securityKey);
} catch (err) {
console.error(err);
this._error = err;
this.emitChange("error");
} finally {
this._isBusy = false;
this.emitChange("isBusy");
}
}
}
}

View file

@ -207,6 +207,10 @@ export class Session {
this.needsSessionBackup.set(false);
}
get sessionBackup() {
return this._sessionBackup;
}
// called after load
async beforeFirstSync(isNewLogin) {
if (this._olm) {

View file

@ -33,6 +33,10 @@ export class SessionBackup {
return JSON.parse(sessionInfo);
}
get version() {
return this._backupInfo.version;
}
dispose() {
this._decryption.free();
}

View file

@ -53,9 +53,9 @@ export async function keyFromCredential(type, credential, storage, cryptoDriver,
throw new Error("Could not find a default secret storage key in account data");
}
let key;
if (type === "passphrase") {
if (type === "phrase") {
key = await keyFromPassphrase(keyDescription, credential, cryptoDriver);
} else if (type === "recoverykey") {
} else if (type === "key") {
key = keyFromRecoveryKey(olm, keyDescription, credential);
} else {
throw new Error(`Invalid type: ${type}`);

View file

@ -306,14 +306,9 @@ a {
}
.SessionStatusView {
padding: 5px;
position: absolute;
top: 20px;
right: 20px;
background-color: #3D88FA;
padding: 8px 4px;
background-color: #03B381;
color: white;
border-radius: 10px;
z-index: 2;
}
.middle-shown .SessionStatusView {
@ -548,7 +543,11 @@ ul.Timeline > li.messageStatus .message-container > p {
}
.SettingsBody {
padding: 12px 16px;
padding: 0px 16px;
}
.Settings h3 {
margin: 16px 0 8px 0;
}
.Settings .row .label {
@ -583,3 +582,20 @@ ul.Timeline > li.messageStatus .message-container > p {
.Settings .row .label {
flex: 0 0 200px;
}
.Settings .error {
color: red;
font-weight: 600;
}
button.link {
font-size: 1em;
border: none;
text-decoration: underline;
background: none;
cursor: pointer;
color: #03B381;
font-weight: 600;
margin: -12px;
padding: 12px;
}

View file

@ -32,3 +32,10 @@ export class SessionStatusView extends TemplateView {
]);
}
}
export class SetupSecretStorageView extends TemplateView {
render(t, vm) {
return t.p([t.a({href: vm.setupSecretStorageUrl}, vm.i18n`Set up secret storage`), vm.i18n` to decrypt older messages.`])
}
}

View file

@ -21,7 +21,7 @@ import {TemplateView} from "../general/TemplateView.js";
import {StaticView} from "../general/StaticView.js";
import {SessionStatusView} from "./SessionStatusView.js";
import {RoomGridView} from "./RoomGridView.js";
import {SettingsView} from "./SettingsView.js";
import {SettingsView} from "./settings/SettingsView.js";
export class SessionView extends TemplateView {
render(t, vm) {

View file

@ -0,0 +1,68 @@
/*
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 {TemplateView} from "../../general/TemplateView.js";
export class SessionBackupSettingsView extends TemplateView {
render(t, vm) {
return t.mapView(vm => vm.status, status => {
switch (status) {
case "enabled": return new TemplateView(vm, renderEnabled)
case "setupKey": return new TemplateView(vm, renderEnableFromKey)
case "setupPhrase": return new TemplateView(vm, renderEnableFromPhrase)
}
});
}
}
function renderEnabled(t, vm) {
return t.p(vm.i18n`Session backup is enabled, using backup version ${vm.backupVersion}.`);
}
function renderEnableFromKey(t, vm) {
return t.div([
t.p(vm.i18n`Enter your 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`Alternatively, you can `, t.button({className: "link", onClick: () => vm.showPhraseSetup()}, vm.i18n`use a security phrase`), vm.i18n` if you have one.`]),
renderError(t),
renderEnableFieldRow(t, vm.i18n`Security key`, key => vm.enterSecurityKey(key))
]);
}
function renderEnableFromPhrase(t, vm) {
return t.div([
t.p(vm.i18n`Enter your 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`You can also `, t.button({className: "link", onClick: () => vm.showKeySetup()}, vm.i18n`use your security key`), vm.i18n`.`]),
renderError(t),
renderEnableFieldRow(t, vm.i18n`Security phrase`, phrase => vm.enterSecurityPhrase(phrase))
]);
}
function renderEnableFieldRow(t, label, callback) {
return t.div({className: `row`}, [
t.div({className: "label"}, label),
t.div({className: "content"}, t.input({type: "password", disabled: vm => vm.isBusy, placeholder: label, onChange: evt => callback(evt.target.value)})),
]);
}
function renderError(t) {
return t.if(vm => vm.error, t.createTemplate((t, vm) => {
return t.div([
t.p({className: "error"}, vm => vm.i18n`Could not enable session 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

@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {TemplateView} from "../general/TemplateView.js";
import {TemplateView} from "../../general/TemplateView.js";
import {SessionBackupSettingsView} from "./SessionBackupSettingsView.js"
export class SettingsView extends TemplateView {
render(t, vm) {
@ -39,9 +40,13 @@ export class SettingsView extends TemplateView {
t.h2("Settings")
]),
t.div({className: "SettingsBody"}, [
t.h3("Session"),
row(vm.i18n`User ID`, vm.userId),
row(vm.i18n`Session ID`, vm.deviceId, "code"),
row(vm.i18n`Session key`, vm.fingerprintKey, "code"),
t.h3("Session Backup"),
t.view(new SessionBackupSettingsView(vm.sessionBackupViewModel)),
t.h3("Application"),
row(vm.i18n`Version`, version),
])
]);