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:
Bruno Windels 2020-10-23 11:10:50 +00:00 committed by GitHub
commit 8125173420
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 61 additions and 25 deletions

View file

@ -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";
}
} }
} }

View file

@ -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;
} }

View file

@ -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

View file

@ -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);

View file

@ -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…`))
} }
}); });
} }