forked from mystiq/hydrogen-web
Merge pull request #174 from vector-im/bwindels/fix-offline-session-backup-init
Fix session backup init breaking offline usage
This commit is contained in:
commit
8125173420
5 changed files with 61 additions and 25 deletions
|
@ -23,6 +23,9 @@ export class SessionBackupViewModel extends ViewModel {
|
||||||
this._showKeySetup = true;
|
this._showKeySetup = true;
|
||||||
this._error = null;
|
this._error = null;
|
||||||
this._isBusy = false;
|
this._isBusy = false;
|
||||||
|
this.track(this._session.hasSecretStorageKey.subscribe(() => {
|
||||||
|
this.emitChange("status");
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
get isBusy() {
|
get isBusy() {
|
||||||
|
@ -37,7 +40,11 @@ export class SessionBackupViewModel extends ViewModel {
|
||||||
if (this._session.sessionBackup) {
|
if (this._session.sessionBackup) {
|
||||||
return "enabled";
|
return "enabled";
|
||||||
} else {
|
} else {
|
||||||
return this._showKeySetup ? "setupKey" : "setupPhrase";
|
if (this._session.hasSecretStorageKey.get() === false) {
|
||||||
|
return this._showKeySetup ? "setupKey" : "setupPhrase";
|
||||||
|
} else {
|
||||||
|
return "pending";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,8 @@ export class Session {
|
||||||
this._getSyncToken = () => this.syncToken;
|
this._getSyncToken = () => this.syncToken;
|
||||||
this._olmWorker = olmWorker;
|
this._olmWorker = olmWorker;
|
||||||
this._cryptoDriver = cryptoDriver;
|
this._cryptoDriver = cryptoDriver;
|
||||||
|
this._sessionBackup = null;
|
||||||
|
this._hasSecretStorageKey = new ObservableValue(null);
|
||||||
|
|
||||||
if (olm) {
|
if (olm) {
|
||||||
this._olmUtil = new olm.Utility();
|
this._olmUtil = new olm.Utility();
|
||||||
|
@ -81,6 +83,10 @@ export class Session {
|
||||||
return this._e2eeAccount?.identityKeys.ed25519;
|
return this._e2eeAccount?.identityKeys.ed25519;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasSecretStorageKey() {
|
||||||
|
return this._hasSecretStorageKey;
|
||||||
|
}
|
||||||
|
|
||||||
get deviceId() {
|
get deviceId() {
|
||||||
return this._sessionInfo.deviceId;
|
return this._sessionInfo.deviceId;
|
||||||
}
|
}
|
||||||
|
@ -92,6 +98,8 @@ export class Session {
|
||||||
// called once this._e2eeAccount is assigned
|
// called once this._e2eeAccount is assigned
|
||||||
_setupEncryption() {
|
_setupEncryption() {
|
||||||
console.log("loaded e2ee account with keys", this._e2eeAccount.identityKeys);
|
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 senderKeyLock = new LockMap();
|
||||||
const olmDecryption = new OlmDecryption({
|
const olmDecryption = new OlmDecryption({
|
||||||
account: this._e2eeAccount,
|
account: this._e2eeAccount,
|
||||||
|
@ -174,6 +182,9 @@ export class Session {
|
||||||
if (!this._olm) {
|
if (!this._olm) {
|
||||||
throw new Error("olm required");
|
throw new Error("olm required");
|
||||||
}
|
}
|
||||||
|
if (this._sessionBackup) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const key = await ssssKeyFromCredential(type, credential, this._storage, this._cryptoDriver, this._olm);
|
const key = await ssssKeyFromCredential(type, credential, this._storage, this._cryptoDriver, this._olm);
|
||||||
// and create session backup, which needs to read from accountData
|
// and create session backup, which needs to read from accountData
|
||||||
const readTxn = this._storage.readTxn([
|
const readTxn = this._storage.readTxn([
|
||||||
|
@ -192,6 +203,7 @@ export class Session {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
await writeTxn.complete();
|
await writeTxn.complete();
|
||||||
|
this._hasSecretStorageKey.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _createSessionBackup(ssssKey, txn) {
|
async _createSessionBackup(ssssKey, txn) {
|
||||||
|
@ -211,12 +223,9 @@ export class Session {
|
||||||
return this._sessionBackup;
|
return this._sessionBackup;
|
||||||
}
|
}
|
||||||
|
|
||||||
// called after load
|
/** @internal */
|
||||||
async beforeFirstSync(isNewLogin) {
|
async createIdentity() {
|
||||||
if (this._olm) {
|
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) {
|
if (!this._e2eeAccount) {
|
||||||
this._e2eeAccount = await E2EEAccount.create({
|
this._e2eeAccount = await E2EEAccount.create({
|
||||||
hsApi: this._hsApi,
|
hsApi: this._hsApi,
|
||||||
|
@ -231,21 +240,10 @@ export class Session {
|
||||||
}
|
}
|
||||||
await this._e2eeAccount.generateOTKsIfNeeded(this._storage);
|
await this._e2eeAccount.generateOTKsIfNeeded(this._storage);
|
||||||
await this._e2eeAccount.uploadKeys(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() {
|
async load() {
|
||||||
const txn = this._storage.readTxn([
|
const txn = this._storage.readTxn([
|
||||||
this._storage.storeNames.session,
|
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) {
|
async start(lastVersionResponse) {
|
||||||
if (lastVersionResponse) {
|
if (lastVersionResponse) {
|
||||||
// store /versions response
|
// store /versions response
|
||||||
|
@ -299,7 +303,21 @@ export class Session {
|
||||||
// TODO: what can we do if this throws?
|
// TODO: what can we do if this throws?
|
||||||
await txn.complete();
|
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([
|
const opsTxn = this._storage.readWriteTxn([
|
||||||
this._storage.storeNames.operations
|
this._storage.storeNames.operations
|
||||||
]);
|
]);
|
||||||
|
@ -333,6 +351,7 @@ export class Session {
|
||||||
return this._rooms;
|
return this._rooms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
createRoom(roomId, pendingEvents) {
|
createRoom(roomId, pendingEvents) {
|
||||||
const room = new Room({
|
const room = new Room({
|
||||||
roomId,
|
roomId,
|
||||||
|
@ -350,6 +369,7 @@ export class Session {
|
||||||
return room;
|
return room;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
async writeSync(syncResponse, syncFilterId, txn) {
|
async writeSync(syncResponse, syncFilterId, txn) {
|
||||||
const changes = {
|
const changes = {
|
||||||
syncInfo: null,
|
syncInfo: null,
|
||||||
|
@ -394,6 +414,7 @@ export class Session {
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
afterSync({syncInfo, e2eeAccountChanges}) {
|
afterSync({syncInfo, e2eeAccountChanges}) {
|
||||||
if (syncInfo) {
|
if (syncInfo) {
|
||||||
// sync transaction succeeded, modify object state now
|
// sync transaction succeeded, modify object state now
|
||||||
|
@ -404,6 +425,7 @@ export class Session {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
async afterSyncCompleted(changes, isCatchupSync) {
|
async afterSyncCompleted(changes, isCatchupSync) {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
if (changes.deviceMessageDecryptionPending) {
|
if (changes.deviceMessageDecryptionPending) {
|
||||||
|
@ -425,10 +447,12 @@ export class Session {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
get syncToken() {
|
get syncToken() {
|
||||||
return this._syncInfo?.token;
|
return this._syncInfo?.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
get syncFilterId() {
|
get syncFilterId() {
|
||||||
return this._syncInfo?.filterId;
|
return this._syncInfo?.filterId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,8 +179,10 @@ export class SessionContainer {
|
||||||
mediaRepository: new MediaRepository(sessionInfo.homeServer)
|
mediaRepository: new MediaRepository(sessionInfo.homeServer)
|
||||||
});
|
});
|
||||||
await this._session.load();
|
await this._session.load();
|
||||||
this._status.set(LoadStatus.SessionSetup);
|
if (isNewLogin) {
|
||||||
await this._session.beforeFirstSync(isNewLogin);
|
this._status.set(LoadStatus.SessionSetup);
|
||||||
|
await this._session.createIdentity();
|
||||||
|
}
|
||||||
|
|
||||||
this._sync = new Sync({hsApi: this._requestScheduler.hsApi, storage: this._storage, session: this._session});
|
this._sync = new Sync({hsApi: this._requestScheduler.hsApi, storage: this._storage, session: this._session});
|
||||||
// notify sync and session when back online
|
// notify sync and session when back online
|
||||||
|
|
|
@ -84,16 +84,17 @@ function isCacheableThumbnail(url) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const baseURL = new URL(self.registration.scope);
|
||||||
async function handleRequest(request) {
|
async function handleRequest(request) {
|
||||||
const baseURL = self.registration.scope;
|
const url = new URL(request.url);
|
||||||
if (request.url === baseURL) {
|
if (url.origin === baseURL.origin && url.pathname === baseURL.pathname) {
|
||||||
request = new Request(new URL("index.html", baseURL));
|
request = new Request(new URL("index.html", baseURL.href));
|
||||||
}
|
}
|
||||||
let response = await readCache(request);
|
let response = await readCache(request);
|
||||||
if (!response) {
|
if (!response) {
|
||||||
// use cors so the resource in the cache isn't opaque and uses up to 7mb
|
// 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
|
// 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"});
|
response = await fetch(request, {mode: "cors", credentials: "omit"});
|
||||||
} else {
|
} else {
|
||||||
response = await fetch(request);
|
response = await fetch(request);
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {TemplateView} from "../../general/TemplateView.js";
|
import {TemplateView} from "../../general/TemplateView.js";
|
||||||
|
import {StaticView} from "../../general/StaticView.js";
|
||||||
|
|
||||||
export class SessionBackupSettingsView extends TemplateView {
|
export class SessionBackupSettingsView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
|
@ -23,6 +24,7 @@ export class SessionBackupSettingsView extends TemplateView {
|
||||||
case "enabled": return new TemplateView(vm, renderEnabled)
|
case "enabled": return new TemplateView(vm, renderEnabled)
|
||||||
case "setupKey": return new TemplateView(vm, renderEnableFromKey)
|
case "setupKey": return new TemplateView(vm, renderEnableFromKey)
|
||||||
case "setupPhrase": return new TemplateView(vm, renderEnableFromPhrase)
|
case "setupPhrase": return new TemplateView(vm, renderEnableFromPhrase)
|
||||||
|
case "pending": return new StaticView(vm, t => t.p(vm.i18n`Waiting to go online…`))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue