This commit is contained in:
Bruno Windels 2021-10-27 15:08:53 +02:00
parent 718b410253
commit c89e414bb5
13 changed files with 190 additions and 79 deletions

View 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);
}
}

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {AccountSetupViewModel} from "./AccountSetupViewModel.js";
import {LoadStatus} from "../matrix/SessionContainer.js"; import {LoadStatus} from "../matrix/SessionContainer.js";
import {SyncStatus} from "../matrix/Sync.js"; import {SyncStatus} from "../matrix/Sync.js";
import {ViewModel} from "./ViewModel.js"; import {ViewModel} from "./ViewModel.js";
@ -29,7 +30,8 @@ export class SessionLoadViewModel extends ViewModel {
this._loading = false; this._loading = false;
this._error = null; this._error = null;
this.backUrl = this.urlCreator.urlForSegment("session", true); this.backUrl = this.urlCreator.urlForSegment("session", true);
this._dehydratedDevice = undefined; this._accountSetupViewModel = undefined;
} }
async start() { async start() {
@ -40,6 +42,11 @@ export class SessionLoadViewModel extends ViewModel {
this._loading = true; this._loading = true;
this.emitChange("loading"); this.emitChange("loading");
this._waitHandle = this._sessionContainer.loadStatus.waitFor(s => { 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"); this.emitChange("loadLabel");
// wait for initial sync, but not catchup sync // wait for initial sync, but not catchup sync
const isCatchupSync = s === LoadStatus.FirstSync && const isCatchupSync = s === LoadStatus.FirstSync &&
@ -98,6 +105,10 @@ export class SessionLoadViewModel extends ViewModel {
// to show a spinner or not // to show a spinner or not
get loading() { get loading() {
const sc = this._sessionContainer;
if (sc && sc.loadStatus.get() === LoadStatus.AccountSetup) {
return false;
}
return this._loading; return this._loading;
} }
@ -113,8 +124,8 @@ export class SessionLoadViewModel extends ViewModel {
switch (sc.loadStatus.get()) { switch (sc.loadStatus.get()) {
case LoadStatus.QueryAccount: case LoadStatus.QueryAccount:
return `Querying account encryption setup…`; return `Querying account encryption setup…`;
case LoadStatus.SetupAccount: case LoadStatus.AccountSetup:
return `Please enter your password to restore your encryption setup`; return `Do you want to restore this dehydrated device?`;
case LoadStatus.SessionSetup: case LoadStatus.SessionSetup:
return `Setting up your encryption keys…`; return `Setting up your encryption keys…`;
case LoadStatus.Loading: case LoadStatus.Loading:
@ -142,26 +153,7 @@ export class SessionLoadViewModel extends ViewModel {
this.platform.saveFileAs(logExport.asBlob(), `hydrogen-logs-${this.platform.clock.now()}.json`); this.platform.saveFileAs(logExport.asBlob(), `hydrogen-logs-${this.platform.clock.now()}.json`);
} }
get canSetupAccount() { get accountSetupViewModel() {
return this._sessionContainer.loadStatus === LoadStatus.SetupAccount; return this._accountSetupViewModel;
}
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);
} }
} }

View file

@ -69,6 +69,10 @@ export class SettingsViewModel extends ViewModel {
}); });
} }
setupDehydratedDevice(key) {
return this._session.setupDehydratedDevice(key);
}
get isLoggingOut() { return this._isLoggingOut; } get isLoggingOut() { return this._isLoggingOut; }
setSentImageSizeLimit(size) { setSentImageSizeLimit(size) {

View file

@ -21,7 +21,6 @@ export class ConsoleLogger extends BaseLogger {
} }
} }
const excludedKeysFromTable = ["l", "id"]; const excludedKeysFromTable = ["l", "id"];
function filterValues(values) { function filterValues(values) {
if (!values) { if (!values) {
@ -79,7 +78,7 @@ function itemCaption(item) {
} else if (item._values.l && item.error) { } else if (item._values.l && item.error) {
return `${item._values.l} failed`; return `${item._values.l} failed`;
} else if (typeof item._values.ref !== "undefined") { } else if (typeof item._values.ref !== "undefined") {
return `ref ${item._values.ref}` return `ref ${item._values.ref}`;
} else { } else {
return item._values.l || item._values.type; return item._values.l || item._values.type;
} }

View file

@ -108,8 +108,7 @@ export class Session {
} }
async logout(log = undefined) { async logout(log = undefined) {
const response = await this._hsApi.logout({log}).response(); await this._hsApi.logout({log}).response();
console.log("logout", response);
} }
// called once this._e2eeAccount is assigned // called once this._e2eeAccount is assigned
@ -249,7 +248,7 @@ export class Session {
async createIdentity(log) { async createIdentity(log) {
if (this._olm) { if (this._olm) {
if (!this._e2eeAccount) { 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); log.set("keys", this._e2eeAccount.identityKeys);
this._setupEncryption(); this._setupEncryption();
} }
@ -259,8 +258,7 @@ export class Session {
} }
/** @internal */ /** @internal */
dehydrateIdentity(dehydratedDevice, log = null) { async dehydrateIdentity(dehydratedDevice, log) {
this._platform.logger.wrapOrRun(log, "dehydrateIdentity", async log => {
log.set("deviceId", dehydratedDevice.deviceId); log.set("deviceId", dehydratedDevice.deviceId);
if (!this._olm) { if (!this._olm) {
log.set("no_olm", true); log.set("no_olm", true);
@ -291,7 +289,6 @@ export class Session {
log.set("keys", this._e2eeAccount.identityKeys); log.set("keys", this._e2eeAccount.identityKeys);
this._setupEncryption(); this._setupEncryption();
return true; return true;
});
} }
_createNewAccount(deviceId, storage = undefined) { _createNewAccount(deviceId, storage = undefined) {

View file

@ -36,7 +36,7 @@ export const LoadStatus = createEnum(
"Login", "Login",
"LoginFailed", "LoginFailed",
"QueryAccount", // check for dehydrated device after login "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", "Loading",
"SessionSetup", // upload e2ee keys, ... "SessionSetup", // upload e2ee keys, ...
"Migrating", // not used atm, but would fit here "Migrating", // not used atm, but would fit here
@ -66,6 +66,7 @@ export class SessionContainer {
this._requestScheduler = null; this._requestScheduler = null;
this._olmPromise = olmPromise; this._olmPromise = olmPromise;
this._workerPromise = workerPromise; this._workerPromise = workerPromise;
this._accountSetup = undefined;
} }
createNewSessionId() { createNewSessionId() {
@ -323,7 +324,7 @@ export class SessionContainer {
let resolveStageFinish; let resolveStageFinish;
const promiseStageFinish = new Promise(r => resolveStageFinish = r); const promiseStageFinish = new Promise(r => resolveStageFinish = r);
this._accountSetup = new AccountSetup(encryptedDehydratedDevice, resolveStageFinish); this._accountSetup = new AccountSetup(encryptedDehydratedDevice, resolveStageFinish);
this._status.set(LoadStatus.SetupAccount); this._status.set(LoadStatus.AccountSetup);
await promiseStageFinish; await promiseStageFinish;
const dehydratedDevice = this._accountSetup?._dehydratedDevice; const dehydratedDevice = this._accountSetup?._dehydratedDevice;
this._accountSetup = null; this._accountSetup = null;

View file

@ -111,7 +111,7 @@ export class Account {
if (!this._areDeviceKeysUploaded) { if (!this._areDeviceKeysUploaded) {
log.set("identity", true); log.set("identity", true);
const identityKeys = JSON.parse(this._account.identity_keys()); 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) { if (oneTimeKeysEntries.length) {
log.set("otks", true); log.set("otks", true);
@ -241,10 +241,10 @@ export class Account {
} }
} }
_deviceKeysPayload(identityKeys) { _deviceKeysPayload(identityKeys, deviceId) {
const obj = { const obj = {
user_id: this._userId, user_id: this._userId,
device_id: this._deviceId, device_id: deviceId,
algorithms: [OLM_ALGORITHM, MEGOLM_ALGORITHM], algorithms: [OLM_ALGORITHM, MEGOLM_ALGORITHM],
keys: {} keys: {}
}; };

View file

@ -54,13 +54,21 @@ class EncryptedDehydratedDevice {
const account = new this._olm.Account(); const account = new this._olm.Account();
try { try {
const pickledAccount = this._dehydratedDevice.device_data.account; const pickledAccount = this._dehydratedDevice.device_data.account;
account.unpickle(key, pickledAccount); account.unpickle(new Uint8Array(key), pickledAccount);
return new DehydratedDevice(this._dehydratedDevice, account); return new DehydratedDevice(this._dehydratedDevice, account);
} catch (err) { } catch (err) {
account.free(); 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 { class DehydratedDevice {

View file

@ -19,7 +19,7 @@ import {encodeQueryParams, encodeBody} from "./common.js";
import {HomeServerRequest} from "./HomeServerRequest.js"; import {HomeServerRequest} from "./HomeServerRequest.js";
const CS_R0_PREFIX = "/_matrix/client/r0"; 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 { export class HomeServerApi {
constructor({homeserver, accessToken, request, reconnector}) { constructor({homeserver, accessToken, request, reconnector}) {
@ -31,7 +31,7 @@ export class HomeServerApi {
this._reconnector = reconnector; this._reconnector = reconnector;
} }
_url(csPath, prefix) { _url(csPath, prefix = CS_R0_PREFIX) {
return this._homeserver + prefix + csPath; return this._homeserver + prefix + csPath;
} }

View file

@ -54,15 +54,12 @@ limitations under the License.
padding: 0 0.4em 0.4em; padding: 0 0.4em 0.4em;
} }
.SessionLoadStatusView, .LoginView_query-spinner { .SessionLoadStatusView > .status, .LoginView_query-spinner {
display: flex; display: flex;
gap: 12px;
} }
.SessionLoadStatusView> :not(:first-child), .LoginView_query-spinner> :not(:first-child) { .SessionLoadStatusView > .status p, .LoginView_query-spinner p {
margin-left: 12px;
}
.SessionLoadStatusView p, .LoginView_query-spinner p {
flex: 1; flex: 1;
margin: 0; margin: 0;
} }

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

View file

@ -16,6 +16,7 @@ limitations under the License.
import {TemplateView} from "../general/TemplateView"; import {TemplateView} from "../general/TemplateView";
import {spinner} from "../common.js"; import {spinner} from "../common.js";
import {AccountSetupView} from "./AccountSetupView.js";
/** a view used both in the login view and the loading screen /** a view used both in the login view and the loading screen
to show the current state of loading the session. to show the current state of loading the session.
@ -28,8 +29,12 @@ export class SessionLoadStatusView extends TemplateView {
}, vm.i18n`Export logs`); }, vm.i18n`Export logs`);
}); });
return t.div({className: "SessionLoadStatusView"}, [ return t.div({className: "SessionLoadStatusView"}, [
spinner(t, {hiddenWithLayout: vm => !vm.loading}), t.p({className: "status"}, [
t.p([vm => vm.loadLabel, exportLogsButtonIfFailed]) spinner(t, {hidden: vm => !vm.loading}),
t.p(vm => vm.loadLabel),
exportLogsButtonIfFailed
]),
t.ifView(vm => vm.accountSetupViewModel, vm => new AccountSetupView(vm.accountSetupViewModel)),
]); ]);
} }
} }

View file

@ -48,7 +48,20 @@ export class SettingsView extends TemplateView {
} }
}, },
disabled: vm => vm.isLoggingOut 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( settingNodes.push(
t.h3("Session Backup"), t.h3("Session Backup"),