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 80f20d0e..e0ee215e 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -62,6 +62,8 @@ export class Session { this._getSyncToken = () => this.syncToken; this._olmWorker = olmWorker; this._cryptoDriver = cryptoDriver; + this._sessionBackup = null; + this._hasSecretStorageKey = new ObservableValue(null); if (olm) { this._olmUtil = new olm.Utility(); @@ -81,6 +83,10 @@ export class Session { return this._e2eeAccount?.identityKeys.ed25519; } + get hasSecretStorageKey() { + return this._hasSecretStorageKey; + } + get deviceId() { return this._sessionInfo.deviceId; } @@ -92,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, @@ -174,6 +182,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([ @@ -192,6 +203,7 @@ export class Session { throw err; } await writeTxn.complete(); + this._hasSecretStorageKey.set(true); } async _createSessionBackup(ssssKey, txn) { @@ -211,12 +223,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,21 +240,10 @@ 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); - } } } + /** @internal */ async load() { const txn = this._storage.readTxn([ this._storage.storeNames.session, @@ -289,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 @@ -299,7 +303,21 @@ 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); + } + this._hasSecretStorageKey.set(!!ssssKey); + } + // restore unfinished operations, like sending out room keys const opsTxn = this._storage.readWriteTxn([ this._storage.storeNames.operations ]); @@ -333,6 +351,7 @@ export class Session { return this._rooms; } + /** @internal */ createRoom(roomId, pendingEvents) { const room = new Room({ roomId, @@ -350,6 +369,7 @@ export class Session { return room; } + /** @internal */ async writeSync(syncResponse, syncFilterId, txn) { const changes = { syncInfo: null, @@ -394,6 +414,7 @@ export class Session { return changes; } + /** @internal */ afterSync({syncInfo, e2eeAccountChanges}) { if (syncInfo) { // sync transaction succeeded, modify object state now @@ -404,6 +425,7 @@ export class Session { } } + /** @internal */ async afterSyncCompleted(changes, isCatchupSync) { const promises = []; if (changes.deviceMessageDecryptionPending) { @@ -425,10 +447,12 @@ export class Session { } } + /** @internal */ get syncToken() { return this._syncInfo?.token; } + /** @internal */ get syncFilterId() { return this._syncInfo?.filterId; } 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 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); 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ā€¦`)) } }); }