From df72e829bf567fb9858f879ac2bf64bf264967d1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 23 Oct 2020 12:22:52 +0200 Subject: [PATCH 1/4] setup session backup as part of start method, so we know we're online also don't upload OTKs in case of existing account until catchup sync has happened --- src/matrix/Session.js | 35 +++++++++++++++++----------------- src/matrix/SessionContainer.js | 6 ++++-- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 80f20d0e..d2d65cf9 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -62,6 +62,7 @@ export class Session { this._getSyncToken = () => this.syncToken; this._olmWorker = olmWorker; this._cryptoDriver = cryptoDriver; + this._sessionBackup = null; if (olm) { this._olmUtil = new olm.Utility(); @@ -211,12 +212,9 @@ export class Session { return this._sessionBackup; } - // called after load - async beforeFirstSync(isNewLogin) { + /** @internal */ + async createIdentity() { if (this._olm) { - if (isNewLogin && this._e2eeAccount) { - throw new Error("there should not be an e2ee account already on a fresh login"); - } if (!this._e2eeAccount) { this._e2eeAccount = await E2EEAccount.create({ hsApi: this._hsApi, @@ -231,18 +229,6 @@ export class Session { } await this._e2eeAccount.generateOTKsIfNeeded(this._storage); await this._e2eeAccount.uploadKeys(this._storage); - await this._deviceMessageHandler.decryptPending(this.rooms); - - const txn = this._storage.readTxn([ - this._storage.storeNames.session, - this._storage.storeNames.accountData, - ]); - // try set up session backup if we stored the ssss key - const ssssKey = await ssssReadKey(txn); - if (ssssKey) { - // txn will end here as this does a network request - await this._createSessionBackup(ssssKey, txn); - } } } @@ -299,7 +285,20 @@ export class Session { // TODO: what can we do if this throws? await txn.complete(); } - + // enable session backup, this requests the latest backup version + if (!this._sessionBackup) { + const txn = this._storage.readTxn([ + this._storage.storeNames.session, + this._storage.storeNames.accountData, + ]); + // try set up session backup if we stored the ssss key + const ssssKey = await ssssReadKey(txn); + if (ssssKey) { + // txn will end here as this does a network request + await this._createSessionBackup(ssssKey, txn); + } + } + // restore unfinished operations, like sending out room keys const opsTxn = this._storage.readWriteTxn([ this._storage.storeNames.operations ]); diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index a9a6dea6..d59d6c49 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -179,8 +179,10 @@ export class SessionContainer { mediaRepository: new MediaRepository(sessionInfo.homeServer) }); await this._session.load(); - this._status.set(LoadStatus.SessionSetup); - await this._session.beforeFirstSync(isNewLogin); + if (isNewLogin) { + this._status.set(LoadStatus.SessionSetup); + await this._session.createIdentity(); + } this._sync = new Sync({hsApi: this._requestScheduler.hsApi, storage: this._storage, session: this._session}); // notify sync and session when back online From df8eed14aab980dadfc4e8b5a69b0e91f74fd0e9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 23 Oct 2020 12:57:47 +0200 Subject: [PATCH 2/4] expose whether we already have a 4s key,to show the 4s setup in settings it's a tri-state of null/false/true with null meaning we need to go online first to know as only then we try to setup session backup --- src/domain/session/settings/SessionBackupViewModel.js | 9 ++++++++- src/matrix/Session.js | 10 ++++++++++ .../web/session/settings/SessionBackupSettingsView.js | 2 ++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/domain/session/settings/SessionBackupViewModel.js b/src/domain/session/settings/SessionBackupViewModel.js index 2fee7b82..d924fae6 100644 --- a/src/domain/session/settings/SessionBackupViewModel.js +++ b/src/domain/session/settings/SessionBackupViewModel.js @@ -23,6 +23,9 @@ export class SessionBackupViewModel extends ViewModel { this._showKeySetup = true; this._error = null; this._isBusy = false; + this.track(this._session.hasSecretStorageKey.subscribe(() => { + this.emitChange("status"); + })); } get isBusy() { @@ -37,7 +40,11 @@ export class SessionBackupViewModel extends ViewModel { if (this._session.sessionBackup) { return "enabled"; } else { - return this._showKeySetup ? "setupKey" : "setupPhrase"; + if (this._session.hasSecretStorageKey.get() === false) { + return this._showKeySetup ? "setupKey" : "setupPhrase"; + } else { + return "pending"; + } } } diff --git a/src/matrix/Session.js b/src/matrix/Session.js index d2d65cf9..f83dda95 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -63,6 +63,7 @@ export class Session { this._olmWorker = olmWorker; this._cryptoDriver = cryptoDriver; this._sessionBackup = null; + this._hasSecretStorageKey = new ObservableValue(null); if (olm) { this._olmUtil = new olm.Utility(); @@ -82,6 +83,10 @@ export class Session { return this._e2eeAccount?.identityKeys.ed25519; } + get hasSecretStorageKey() { + return this._hasSecretStorageKey; + } + get deviceId() { return this._sessionInfo.deviceId; } @@ -175,6 +180,9 @@ export class Session { if (!this._olm) { throw new Error("olm required"); } + if (this._sessionBackup) { + return false; + } const key = await ssssKeyFromCredential(type, credential, this._storage, this._cryptoDriver, this._olm); // and create session backup, which needs to read from accountData const readTxn = this._storage.readTxn([ @@ -193,6 +201,7 @@ export class Session { throw err; } await writeTxn.complete(); + this._hasSecretStorageKey.set(true); } async _createSessionBackup(ssssKey, txn) { @@ -297,6 +306,7 @@ export class Session { // txn will end here as this does a network request await this._createSessionBackup(ssssKey, txn); } + this._hasSecretStorageKey.set(!!ssssKey); } // restore unfinished operations, like sending out room keys const opsTxn = this._storage.readWriteTxn([ diff --git a/src/ui/web/session/settings/SessionBackupSettingsView.js b/src/ui/web/session/settings/SessionBackupSettingsView.js index 162f2525..c1c0e455 100644 --- a/src/ui/web/session/settings/SessionBackupSettingsView.js +++ b/src/ui/web/session/settings/SessionBackupSettingsView.js @@ -15,6 +15,7 @@ limitations under the License. */ import {TemplateView} from "../../general/TemplateView.js"; +import {StaticView} from "../../general/StaticView.js"; export class SessionBackupSettingsView extends TemplateView { render(t, vm) { @@ -23,6 +24,7 @@ export class SessionBackupSettingsView extends TemplateView { case "enabled": return new TemplateView(vm, renderEnabled) case "setupKey": return new TemplateView(vm, renderEnableFromKey) case "setupPhrase": return new TemplateView(vm, renderEnableFromPhrase) + case "pending": return new StaticView(vm, t => t.p(vm.i18n`Waiting to go onlineā€¦`)) } }); } From 62c8d6574c8b811f78fe7c6784a6307982d34dc8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 23 Oct 2020 12:58:45 +0200 Subject: [PATCH 3/4] fix index.html not being restored from cache when offline on #hash url --- src/service-worker.template.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/service-worker.template.js b/src/service-worker.template.js index 52d508ff..903589a8 100644 --- a/src/service-worker.template.js +++ b/src/service-worker.template.js @@ -84,16 +84,17 @@ function isCacheableThumbnail(url) { return false; } +const baseURL = new URL(self.registration.scope); async function handleRequest(request) { - const baseURL = self.registration.scope; - if (request.url === baseURL) { - request = new Request(new URL("index.html", baseURL)); + const url = new URL(request.url); + if (url.origin === baseURL.origin && url.pathname === baseURL.pathname) { + request = new Request(new URL("index.html", baseURL.href)); } let response = await readCache(request); if (!response) { // use cors so the resource in the cache isn't opaque and uses up to 7mb // https://developers.google.com/web/tools/chrome-devtools/progressive-web-apps?utm_source=devtools#opaque-responses - if (isCacheableThumbnail(new URL(request.url))) { + if (isCacheableThumbnail(url)) { response = await fetch(request, {mode: "cors", credentials: "omit"}); } else { response = await fetch(request); From f214c53fa96f8fda4d7de1326e409e3a4e2a6b55 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 23 Oct 2020 12:59:40 +0200 Subject: [PATCH 4/4] add @internal and other comments to Session --- src/matrix/Session.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index f83dda95..e0ee215e 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -98,6 +98,8 @@ export class Session { // called once this._e2eeAccount is assigned _setupEncryption() { console.log("loaded e2ee account with keys", this._e2eeAccount.identityKeys); + // TODO: this should all go in a wrapper in e2ee/ that is bootstrapped by passing in the account + // and can create RoomEncryption objects and handle encrypted to_device messages and device list changes. const senderKeyLock = new LockMap(); const olmDecryption = new OlmDecryption({ account: this._e2eeAccount, @@ -241,6 +243,7 @@ export class Session { } } + /** @internal */ async load() { const txn = this._storage.readTxn([ this._storage.storeNames.session, @@ -284,6 +287,12 @@ export class Session { } } + /** + * @internal called when coming back online + * @param {Object} lastVersionResponse a response from /versions, which is polled while offline, + * and useful to store so we can later tell what capabilities + * our homeserver has. + */ async start(lastVersionResponse) { if (lastVersionResponse) { // store /versions response @@ -342,6 +351,7 @@ export class Session { return this._rooms; } + /** @internal */ createRoom(roomId, pendingEvents) { const room = new Room({ roomId, @@ -359,6 +369,7 @@ export class Session { return room; } + /** @internal */ async writeSync(syncResponse, syncFilterId, txn) { const changes = { syncInfo: null, @@ -403,6 +414,7 @@ export class Session { return changes; } + /** @internal */ afterSync({syncInfo, e2eeAccountChanges}) { if (syncInfo) { // sync transaction succeeded, modify object state now @@ -413,6 +425,7 @@ export class Session { } } + /** @internal */ async afterSyncCompleted(changes, isCatchupSync) { const promises = []; if (changes.deviceMessageDecryptionPending) { @@ -434,10 +447,12 @@ export class Session { } } + /** @internal */ get syncToken() { return this._syncInfo?.token; } + /** @internal */ get syncFilterId() { return this._syncInfo?.filterId; }