diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js index 6a63145f..75ecfec1 100644 --- a/src/domain/SessionLoadViewModel.js +++ b/src/domain/SessionLoadViewModel.js @@ -78,7 +78,7 @@ export class SessionLoadViewModel extends ViewModel { this._ready(client); } if (loadError) { - console.error("session load error", loadError); + console.error("session load error", loadError.stack); } } catch (err) { this._error = err; diff --git a/src/domain/session/settings/KeyBackupViewModel.ts b/src/domain/session/settings/KeyBackupViewModel.ts index cdfd4081..3426191b 100644 --- a/src/domain/session/settings/KeyBackupViewModel.ts +++ b/src/domain/session/settings/KeyBackupViewModel.ts @@ -56,7 +56,7 @@ export class KeyBackupViewModel extends ViewModel { super(options); const onKeyBackupSet = (keyBackup: KeyBackup | undefined) => { if (keyBackup && !this._keyBackupSubscription) { - this._keyBackupSubscription = this.track(this._session.keyBackup.disposableOn("change", () => { + this._keyBackupSubscription = this.track(this._session.keyBackup.get().disposableOn("change", () => { this._onKeyBackupChange(); })); } else if (!keyBackup && this._keyBackupSubscription) { diff --git a/src/matrix/Session.js b/src/matrix/Session.js index b10b2824..65555286 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -254,7 +254,7 @@ export class Session { } // TODO: stop cross-signing const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); - if (await this._tryLoadSecretStorage(key, undefined, log)) { + if (await this._tryLoadSecretStorage(key, log)) { // only after having read a secret, write the key // as we only find out if it was good if the MAC verification succeeds await this._writeSSSSKey(key, log); @@ -318,24 +318,19 @@ export class Session { // TODO: stop cross-signing } - _tryLoadSecretStorage(ssssKey, existingTxn, log) { + _tryLoadSecretStorage(ssssKey, log) { return log.wrap("enable secret storage", async log => { - const txn = existingTxn ?? await this._storage.readTxn([ - this._storage.storeNames.accountData, - this._storage.storeNames.crossSigningKeys, - this._storage.storeNames.userIdentities, - ]); - const secretStorage = new SecretStorage({key: ssssKey, platform: this._platform}); - const isValid = await secretStorage.hasValidKeyForAnyAccountData(txn); + const secretStorage = new SecretStorage({key: ssssKey, platform: this._platform, storage: this._storage}); + const isValid = await secretStorage.hasValidKeyForAnyAccountData(); log.set("isValid", isValid); if (isValid) { - await this._loadSecretStorageServices(secretStorage, txn, log); + await this._loadSecretStorageServices(secretStorage, log); } return isValid; }); } - async _loadSecretStorageServices(secretStorage, txn, log) { + async _loadSecretStorageServices(secretStorage, log) { try { await log.wrap("enable key backup", async log => { const keyBackup = new KeyBackup( @@ -345,7 +340,7 @@ export class Session { this._storage, this._platform, ); - if (await keyBackup.load(secretStorage, txn)) { + if (await keyBackup.load(secretStorage, log)) { for (const room of this._rooms.values()) { if (room.isEncrypted) { room.enableKeyBackup(keyBackup); @@ -370,7 +365,7 @@ export class Session { ownUserId: this.userId, e2eeAccount: this._e2eeAccount }); - if (await crossSigning.load(txn, log)) { + if (await crossSigning.load(log)) { this._crossSigning.set(crossSigning); } }); @@ -497,15 +492,8 @@ export class Session { olmWorker: this._olmWorker, txn }); - if (this._e2eeAccount) { - log.set("keys", this._e2eeAccount.identityKeys); - this._setupEncryption(); - // try set up session backup if we stored the ssss key - const ssssKey = await ssssReadKey(txn); - if (ssssKey) { - await this._tryLoadSecretStorage(ssssKey, txn, log); - } - } + log.set("keys", this._e2eeAccount.identityKeys); + this._setupEncryption(); } const pendingEventsByRoomId = await this._getPendingEventsByRoom(txn); // load invites @@ -530,6 +518,14 @@ export class Session { room.setInvite(invite); } } + if (this._olm && this._e2eeAccount) { + // try set up session backup and cross-signing if we stored the ssss key + const ssssKey = await ssssReadKey(txn); + if (ssssKey) { + // this will close the txn above, so we do it last + await this._tryLoadSecretStorage(ssssKey, log); + } + } } dispose() { @@ -570,15 +566,15 @@ export class Session { await log.wrap("SSSSKeyFromDehydratedDeviceKey", async log => { const ssssKey = await createSSSSKeyFromDehydratedDeviceKey(dehydratedDevice.key, this._storage, this._platform); if (ssssKey) { - if (await this._tryLoadSecretStorage(ssssKey, undefined, log)) { + if (await this._tryLoadSecretStorage(ssssKey, log)) { log.set("success", true); await this._writeSSSSKey(ssssKey); } } }); } - this._keyBackup.get()?.start(log); - this._crossSigning.get()?.start(log); + await this._keyBackup.get()?.start(log); + await this._crossSigning.get()?.start(log); // restore unfinished operations, like sending out room keys const opsTxn = await this._storage.readWriteTxn([ diff --git a/src/matrix/e2ee/DeviceTracker.ts b/src/matrix/e2ee/DeviceTracker.ts index 23bdc31e..3a50a890 100644 --- a/src/matrix/e2ee/DeviceTracker.ts +++ b/src/matrix/e2ee/DeviceTracker.ts @@ -163,9 +163,9 @@ export class DeviceTracker { } } - async getCrossSigningKeyForUser(userId: string, usage: KeyUsage, hsApi: HomeServerApi | undefined, existingTxn: Transaction | undefined, log: ILogItem): Promise { + async getCrossSigningKeyForUser(userId: string, usage: KeyUsage, hsApi: HomeServerApi | undefined, log: ILogItem): Promise { return await log.wrap({l: "DeviceTracker.getCrossSigningKeyForUser", id: userId, usage}, async log => { - const txn = existingTxn ?? await this._storage.readTxn([ + const txn = await this._storage.readTxn([ this._storage.storeNames.userIdentities, this._storage.storeNames.crossSigningKeys, ]); diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index 8e9a4a81..da107502 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -107,9 +107,8 @@ export class KeyBackup extends EventEmitter<{change: never}> { return txn.inboundGroupSessions.markAllAsNotBackedUp(); } - async load(secretStorage: SecretStorage, txn: Transaction) { - // TODO: no exception here we should anticipate? - const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1", txn); + async load(secretStorage: SecretStorage, log: ILogItem) { + const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1"); if (base64PrivateKey) { this.privateKey = new Uint8Array(this.platform.encoding.base64.decode(base64PrivateKey)); return true; diff --git a/src/matrix/ssss/SecretStorage.ts b/src/matrix/ssss/SecretStorage.ts index 093382a8..4c767bbb 100644 --- a/src/matrix/ssss/SecretStorage.ts +++ b/src/matrix/ssss/SecretStorage.ts @@ -40,22 +40,22 @@ class DecryptionError extends Error { export class SecretStorage { private readonly _key: Key; private readonly _platform: Platform; + private readonly _storage: Storage; - constructor({key, platform}: {key: Key, platform: Platform}) { + constructor({key, platform, storage}: {key: Key, platform: Platform, storage: Storage}) { this._key = key; this._platform = platform; + this._storage = storage; } - async hasValidKeyForAnyAccountData(txn: Transaction) { + /** this method will auto-commit any indexeddb transaction because of its use of the webcrypto api */ + async hasValidKeyForAnyAccountData() { + const txn = await this._storage.readTxn([ + this._storage.storeNames.accountData, + ]); const allAccountData = await txn.accountData.getAll(); for (const accountData of allAccountData) { try { - // TODO: fix this, using the webcrypto api closes the transaction - if (accountData.type === "m.megolm_backup.v1") { - return true; - } else { - continue; - } const secret = await this._decryptAccountData(accountData); return true; // decryption succeeded } catch (err) { @@ -69,7 +69,11 @@ export class SecretStorage { return false; } - async readSecret(name: string, txn: Transaction): Promise { + /** this method will auto-commit any indexeddb transaction because of its use of the webcrypto api */ + async readSecret(name: string): Promise { + const txn = await this._storage.readTxn([ + this._storage.storeNames.accountData, + ]); const accountData = await txn.accountData.get(name); if (!accountData) { return; diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index 1abc3702..196190ae 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -99,22 +99,22 @@ export class CrossSigning { this.e2eeAccount = options.e2eeAccount } - async load(txn: Transaction, log: ILogItem) { + async load(log: ILogItem) { // try to verify the msk without accessing the network - return await this.verifyMSKFrom4S(undefined, txn, log); + return await this.verifyMSKFrom4S(false, log); } async start(log: ILogItem) { if (!this.isMasterKeyTrusted) { // try to verify the msk _with_ access to the network - return await this.verifyMSKFrom4S(this.hsApi, undefined, log); + return await this.verifyMSKFrom4S(true, log); } } - private async verifyMSKFrom4S(hsApi: HomeServerApi | undefined, txn: Transaction | undefined, log: ILogItem): Promise { + private async verifyMSKFrom4S(allowNetwork: boolean, log: ILogItem): Promise { return await log.wrap("CrossSigning.verifyMSKFrom4S", async log => { // TODO: use errorboundary here - const privateMasterKey = await this.getSigningKey(KeyUsage.Master, txn); + const privateMasterKey = await this.getSigningKey(KeyUsage.Master); if (!privateMasterKey) { return false; } @@ -125,7 +125,7 @@ export class CrossSigning { } finally { signing.free(); } - const publishedMasterKey = await this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, hsApi, txn, log); + const publishedMasterKey = await this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, allowNetwork ? this.hsApi : undefined, log); if (!publishedMasterKey) { return false; } @@ -210,11 +210,11 @@ export class CrossSigning { if (!this.isMasterKeyTrusted) { return UserTrust.OwnSetupError; } - const ourMSK = await log.wrap("get our msk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, txn, log)); + const ourMSK = await log.wrap("get our msk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, undefined, log)); if (!ourMSK) { return UserTrust.OwnSetupError; } - const ourUSK = await log.wrap("get our usk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.UserSigning, this.hsApi, txn, log)); + const ourUSK = await log.wrap("get our usk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.UserSigning, this.hsApi, undefined, log)); if (!ourUSK) { return UserTrust.OwnSetupError; } @@ -222,7 +222,7 @@ export class CrossSigning { if (ourUSKVerification !== SignatureVerification.Valid) { return UserTrust.OwnSetupError; } - const theirMSK = await log.wrap("get their msk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, txn, log)); + const theirMSK = await log.wrap("get their msk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, undefined, log)); if (!theirMSK) { /* assume that when they don't have an MSK, they've never enabled cross-signing on their client (or it's not supported) rather than assuming a setup error on their side. @@ -237,7 +237,7 @@ export class CrossSigning { return UserTrust.UserSignatureMismatch; } } - const theirSSK = await log.wrap("get their ssk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.SelfSigning, this.hsApi, txn, log)); + const theirSSK = await log.wrap("get their ssk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.SelfSigning, this.hsApi, undefined, log)); if (!theirSSK) { return UserTrust.UserSetupError; } @@ -290,11 +290,8 @@ export class CrossSigning { return keyToSign; } - private async getSigningKey(usage: KeyUsage, existingTxn?: Transaction): Promise { - const txn = existingTxn ?? await this.storage.readTxn([ - this.storage.storeNames.accountData, - ]); - const seedStr = await this.secretStorage.readSecret(`m.cross_signing.${usage}`, txn); + private async getSigningKey(usage: KeyUsage): Promise { + const seedStr = await this.secretStorage.readSecret(`m.cross_signing.${usage}`); if (seedStr) { return new Uint8Array(this.platform.encoding.base64.decode(seedStr)); }