WIP3
This commit is contained in:
parent
718b410253
commit
c89e414bb5
13 changed files with 190 additions and 79 deletions
49
src/domain/AccountSetupViewModel.js
Normal file
49
src/domain/AccountSetupViewModel.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
Copyright 2021 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 {ViewModel} from "./ViewModel.js";
|
||||
|
||||
export class AccountSetupViewModel extends ViewModel {
|
||||
constructor(accountSetup) {
|
||||
super();
|
||||
this._accountSetup = accountSetup;
|
||||
this._dehydratedDevice = undefined;
|
||||
}
|
||||
|
||||
get canDehydrateDevice() {
|
||||
return !!this._accountSetup.encryptedDehydratedDevice;
|
||||
}
|
||||
|
||||
get deviceDecrypted() {
|
||||
return !!this._dehydratedDevice;
|
||||
}
|
||||
|
||||
get dehydratedDeviceId() {
|
||||
return this._accountSetup.encryptedDehydratedDevice.deviceId;
|
||||
}
|
||||
|
||||
tryDecryptDehydratedDevice(password) {
|
||||
const {encryptedDehydratedDevice} = this._accountSetup;
|
||||
if (encryptedDehydratedDevice) {
|
||||
this._dehydratedDevice = encryptedDehydratedDevice.decrypt(password);
|
||||
this.emitChange("deviceDecrypted");
|
||||
}
|
||||
}
|
||||
|
||||
finish() {
|
||||
this._accountSetup.finish(this._dehydratedDevice);
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {AccountSetupViewModel} from "./AccountSetupViewModel.js";
|
||||
import {LoadStatus} from "../matrix/SessionContainer.js";
|
||||
import {SyncStatus} from "../matrix/Sync.js";
|
||||
import {ViewModel} from "./ViewModel.js";
|
||||
|
@ -29,7 +30,8 @@ export class SessionLoadViewModel extends ViewModel {
|
|||
this._loading = false;
|
||||
this._error = null;
|
||||
this.backUrl = this.urlCreator.urlForSegment("session", true);
|
||||
this._dehydratedDevice = undefined;
|
||||
this._accountSetupViewModel = undefined;
|
||||
|
||||
}
|
||||
|
||||
async start() {
|
||||
|
@ -40,6 +42,11 @@ export class SessionLoadViewModel extends ViewModel {
|
|||
this._loading = true;
|
||||
this.emitChange("loading");
|
||||
this._waitHandle = this._sessionContainer.loadStatus.waitFor(s => {
|
||||
if (s === LoadStatus.AccountSetup) {
|
||||
this._accountSetupViewModel = new AccountSetupViewModel(this._sessionContainer.accountSetup);
|
||||
} else {
|
||||
this._accountSetupViewModel = undefined;
|
||||
}
|
||||
this.emitChange("loadLabel");
|
||||
// wait for initial sync, but not catchup sync
|
||||
const isCatchupSync = s === LoadStatus.FirstSync &&
|
||||
|
@ -98,6 +105,10 @@ export class SessionLoadViewModel extends ViewModel {
|
|||
|
||||
// to show a spinner or not
|
||||
get loading() {
|
||||
const sc = this._sessionContainer;
|
||||
if (sc && sc.loadStatus.get() === LoadStatus.AccountSetup) {
|
||||
return false;
|
||||
}
|
||||
return this._loading;
|
||||
}
|
||||
|
||||
|
@ -113,8 +124,8 @@ export class SessionLoadViewModel extends ViewModel {
|
|||
switch (sc.loadStatus.get()) {
|
||||
case LoadStatus.QueryAccount:
|
||||
return `Querying account encryption setup…`;
|
||||
case LoadStatus.SetupAccount:
|
||||
return `Please enter your password to restore your encryption setup`;
|
||||
case LoadStatus.AccountSetup:
|
||||
return `Do you want to restore this dehydrated device?`;
|
||||
case LoadStatus.SessionSetup:
|
||||
return `Setting up your encryption keys…`;
|
||||
case LoadStatus.Loading:
|
||||
|
@ -142,26 +153,7 @@ export class SessionLoadViewModel extends ViewModel {
|
|||
this.platform.saveFileAs(logExport.asBlob(), `hydrogen-logs-${this.platform.clock.now()}.json`);
|
||||
}
|
||||
|
||||
get canSetupAccount() {
|
||||
return this._sessionContainer.loadStatus === LoadStatus.SetupAccount;
|
||||
}
|
||||
|
||||
get canDehydrateDevice() {
|
||||
return this.canSetupAccount && !!this._sessionContainer.accountSetup.encryptedDehydratedDevice;
|
||||
}
|
||||
|
||||
tryDecryptDehydratedDevice(password) {
|
||||
const {encryptedDehydratedDevice} = this._sessionContainer.accountSetup;
|
||||
if (encryptedDehydratedDevice) {
|
||||
this._dehydratedDevice = encryptedDehydratedDevice.decrypt(password);
|
||||
return !!this._dehydratedDevice;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
finishAccountSetup() {
|
||||
const dehydratedDevice = this._dehydratedDevice;
|
||||
this._dehydratedDevice = undefined;
|
||||
this._sessionContainer.accountSetup.finish(dehydratedDevice);
|
||||
get accountSetupViewModel() {
|
||||
return this._accountSetupViewModel;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,6 +69,10 @@ export class SettingsViewModel extends ViewModel {
|
|||
});
|
||||
}
|
||||
|
||||
setupDehydratedDevice(key) {
|
||||
return this._session.setupDehydratedDevice(key);
|
||||
}
|
||||
|
||||
get isLoggingOut() { return this._isLoggingOut; }
|
||||
|
||||
setSentImageSizeLimit(size) {
|
||||
|
|
|
@ -21,7 +21,6 @@ export class ConsoleLogger extends BaseLogger {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
const excludedKeysFromTable = ["l", "id"];
|
||||
function filterValues(values) {
|
||||
if (!values) {
|
||||
|
@ -79,7 +78,7 @@ function itemCaption(item) {
|
|||
} else if (item._values.l && item.error) {
|
||||
return `${item._values.l} failed`;
|
||||
} else if (typeof item._values.ref !== "undefined") {
|
||||
return `ref ${item._values.ref}`
|
||||
return `ref ${item._values.ref}`;
|
||||
} else {
|
||||
return item._values.l || item._values.type;
|
||||
}
|
||||
|
|
|
@ -108,8 +108,7 @@ export class Session {
|
|||
}
|
||||
|
||||
async logout(log = undefined) {
|
||||
const response = await this._hsApi.logout({log}).response();
|
||||
console.log("logout", response);
|
||||
await this._hsApi.logout({log}).response();
|
||||
}
|
||||
|
||||
// called once this._e2eeAccount is assigned
|
||||
|
@ -249,7 +248,7 @@ export class Session {
|
|||
async createIdentity(log) {
|
||||
if (this._olm) {
|
||||
if (!this._e2eeAccount) {
|
||||
this._e2eeAccount = this._createNewAccount(this._sessionInfo.deviceId, this._storage);
|
||||
this._e2eeAccount = await this._createNewAccount(this._sessionInfo.deviceId, this._storage);
|
||||
log.set("keys", this._e2eeAccount.identityKeys);
|
||||
this._setupEncryption();
|
||||
}
|
||||
|
@ -259,8 +258,7 @@ export class Session {
|
|||
}
|
||||
|
||||
/** @internal */
|
||||
dehydrateIdentity(dehydratedDevice, log = null) {
|
||||
this._platform.logger.wrapOrRun(log, "dehydrateIdentity", async log => {
|
||||
async dehydrateIdentity(dehydratedDevice, log) {
|
||||
log.set("deviceId", dehydratedDevice.deviceId);
|
||||
if (!this._olm) {
|
||||
log.set("no_olm", true);
|
||||
|
@ -291,7 +289,6 @@ export class Session {
|
|||
log.set("keys", this._e2eeAccount.identityKeys);
|
||||
this._setupEncryption();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
_createNewAccount(deviceId, storage = undefined) {
|
||||
|
|
|
@ -36,7 +36,7 @@ export const LoadStatus = createEnum(
|
|||
"Login",
|
||||
"LoginFailed",
|
||||
"QueryAccount", // check for dehydrated device after login
|
||||
"SetupAccount", // asked to restore from dehydrated device if present, call sc.accountSetup.finish() to progress to the next stage
|
||||
"AccountSetup", // asked to restore from dehydrated device if present, call sc.accountSetup.finish() to progress to the next stage
|
||||
"Loading",
|
||||
"SessionSetup", // upload e2ee keys, ...
|
||||
"Migrating", // not used atm, but would fit here
|
||||
|
@ -66,6 +66,7 @@ export class SessionContainer {
|
|||
this._requestScheduler = null;
|
||||
this._olmPromise = olmPromise;
|
||||
this._workerPromise = workerPromise;
|
||||
this._accountSetup = undefined;
|
||||
}
|
||||
|
||||
createNewSessionId() {
|
||||
|
@ -323,7 +324,7 @@ export class SessionContainer {
|
|||
let resolveStageFinish;
|
||||
const promiseStageFinish = new Promise(r => resolveStageFinish = r);
|
||||
this._accountSetup = new AccountSetup(encryptedDehydratedDevice, resolveStageFinish);
|
||||
this._status.set(LoadStatus.SetupAccount);
|
||||
this._status.set(LoadStatus.AccountSetup);
|
||||
await promiseStageFinish;
|
||||
const dehydratedDevice = this._accountSetup?._dehydratedDevice;
|
||||
this._accountSetup = null;
|
||||
|
|
|
@ -111,7 +111,7 @@ export class Account {
|
|||
if (!this._areDeviceKeysUploaded) {
|
||||
log.set("identity", true);
|
||||
const identityKeys = JSON.parse(this._account.identity_keys());
|
||||
payload.device_keys = this._deviceKeysPayload(identityKeys);
|
||||
payload.device_keys = this._deviceKeysPayload(identityKeys, dehydratedDeviceId || this._deviceId);
|
||||
}
|
||||
if (oneTimeKeysEntries.length) {
|
||||
log.set("otks", true);
|
||||
|
@ -241,10 +241,10 @@ export class Account {
|
|||
}
|
||||
}
|
||||
|
||||
_deviceKeysPayload(identityKeys) {
|
||||
_deviceKeysPayload(identityKeys, deviceId) {
|
||||
const obj = {
|
||||
user_id: this._userId,
|
||||
device_id: this._deviceId,
|
||||
device_id: deviceId,
|
||||
algorithms: [OLM_ALGORITHM, MEGOLM_ALGORITHM],
|
||||
keys: {}
|
||||
};
|
||||
|
|
|
@ -54,13 +54,21 @@ class EncryptedDehydratedDevice {
|
|||
const account = new this._olm.Account();
|
||||
try {
|
||||
const pickledAccount = this._dehydratedDevice.device_data.account;
|
||||
account.unpickle(key, pickledAccount);
|
||||
account.unpickle(new Uint8Array(key), pickledAccount);
|
||||
return new DehydratedDevice(this._dehydratedDevice, account);
|
||||
} catch (err) {
|
||||
account.free();
|
||||
return null;
|
||||
if (err.message === "OLM.BAD_ACCOUNT_KEY") {
|
||||
return undefined;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get deviceId() {
|
||||
return this._dehydratedDevice.device_id;
|
||||
}
|
||||
}
|
||||
|
||||
class DehydratedDevice {
|
||||
|
|
|
@ -19,7 +19,7 @@ import {encodeQueryParams, encodeBody} from "./common.js";
|
|||
import {HomeServerRequest} from "./HomeServerRequest.js";
|
||||
|
||||
const CS_R0_PREFIX = "/_matrix/client/r0";
|
||||
const DEHYDRATION_PREFIX = "/unstable/org.matrix.msc2697.v2";
|
||||
const DEHYDRATION_PREFIX = "/_matrix/client/unstable/org.matrix.msc2697.v2";
|
||||
|
||||
export class HomeServerApi {
|
||||
constructor({homeserver, accessToken, request, reconnector}) {
|
||||
|
@ -31,7 +31,7 @@ export class HomeServerApi {
|
|||
this._reconnector = reconnector;
|
||||
}
|
||||
|
||||
_url(csPath, prefix) {
|
||||
_url(csPath, prefix = CS_R0_PREFIX) {
|
||||
return this._homeserver + prefix + csPath;
|
||||
}
|
||||
|
||||
|
|
|
@ -54,15 +54,12 @@ limitations under the License.
|
|||
padding: 0 0.4em 0.4em;
|
||||
}
|
||||
|
||||
.SessionLoadStatusView, .LoginView_query-spinner {
|
||||
.SessionLoadStatusView > .status, .LoginView_query-spinner {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.SessionLoadStatusView> :not(:first-child), .LoginView_query-spinner> :not(:first-child) {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.SessionLoadStatusView p, .LoginView_query-spinner p {
|
||||
.SessionLoadStatusView > .status p, .LoginView_query-spinner p {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
46
src/platform/web/ui/login/AccountSetupView.js
Normal file
46
src/platform/web/ui/login/AccountSetupView.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
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";
|
||||
|
||||
export class AccountSetupView extends TemplateView {
|
||||
render(t, vm) {
|
||||
const keyLabel = vm => `Dehydration key for device ${vm.dehydratedDeviceId}`;
|
||||
const password = t.input({
|
||||
id: "dehydrated_device_key",
|
||||
type: "password",
|
||||
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);
|
||||
}
|
||||
}, [
|
||||
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`),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
import {TemplateView} from "../general/TemplateView";
|
||||
import {spinner} from "../common.js";
|
||||
import {AccountSetupView} from "./AccountSetupView.js";
|
||||
|
||||
/** a view used both in the login view and the loading screen
|
||||
to show the current state of loading the session.
|
||||
|
@ -28,8 +29,12 @@ export class SessionLoadStatusView extends TemplateView {
|
|||
}, vm.i18n`Export logs`);
|
||||
});
|
||||
return t.div({className: "SessionLoadStatusView"}, [
|
||||
spinner(t, {hiddenWithLayout: vm => !vm.loading}),
|
||||
t.p([vm => vm.loadLabel, exportLogsButtonIfFailed])
|
||||
t.p({className: "status"}, [
|
||||
spinner(t, {hidden: vm => !vm.loading}),
|
||||
t.p(vm => vm.loadLabel),
|
||||
exportLogsButtonIfFailed
|
||||
]),
|
||||
t.ifView(vm => vm.accountSetupViewModel, vm => new AccountSetupView(vm.accountSetupViewModel)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,20 @@ export class SettingsView extends TemplateView {
|
|||
}
|
||||
},
|
||||
disabled: vm => vm.isLoggingOut
|
||||
}, vm.i18n`Log out`))
|
||||
}, vm.i18n`Log out`)),
|
||||
row(t, "", t.button({
|
||||
onClick: async () => {
|
||||
const key = prompt(vm.i18n`Enter the key to encrypt the dehydrated device`);
|
||||
if (key) {
|
||||
try {
|
||||
const deviceId = await vm.setupDehydratedDevice(key);
|
||||
alert(`Successfully set up dehydrated device with id ${deviceId}`);
|
||||
} catch (err) {
|
||||
alert(`Failed to set up dehydrated device: ${err.message}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
}, vm.i18n`Set up dehydrated device`))
|
||||
);
|
||||
settingNodes.push(
|
||||
t.h3("Session Backup"),
|
||||
|
|
Reference in a new issue