don't reuse existing transaction to read from 4S, as webcrypto terminates idb transactions

This commit is contained in:
Bruno Windels 2023-03-24 13:42:19 +01:00
parent dd59f37dce
commit 762a91bd16
7 changed files with 52 additions and 56 deletions

View File

@ -78,7 +78,7 @@ export class SessionLoadViewModel extends ViewModel {
this._ready(client); this._ready(client);
} }
if (loadError) { if (loadError) {
console.error("session load error", loadError); console.error("session load error", loadError.stack);
} }
} catch (err) { } catch (err) {
this._error = err; this._error = err;

View File

@ -56,7 +56,7 @@ export class KeyBackupViewModel extends ViewModel<SegmentType, Options> {
super(options); super(options);
const onKeyBackupSet = (keyBackup: KeyBackup | undefined) => { const onKeyBackupSet = (keyBackup: KeyBackup | undefined) => {
if (keyBackup && !this._keyBackupSubscription) { 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(); this._onKeyBackupChange();
})); }));
} else if (!keyBackup && this._keyBackupSubscription) { } else if (!keyBackup && this._keyBackupSubscription) {

View File

@ -254,7 +254,7 @@ export class Session {
} }
// TODO: stop cross-signing // TODO: stop cross-signing
const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); 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 // only after having read a secret, write the key
// as we only find out if it was good if the MAC verification succeeds // as we only find out if it was good if the MAC verification succeeds
await this._writeSSSSKey(key, log); await this._writeSSSSKey(key, log);
@ -318,24 +318,19 @@ export class Session {
// TODO: stop cross-signing // TODO: stop cross-signing
} }
_tryLoadSecretStorage(ssssKey, existingTxn, log) { _tryLoadSecretStorage(ssssKey, log) {
return log.wrap("enable secret storage", async log => { return log.wrap("enable secret storage", async log => {
const txn = existingTxn ?? await this._storage.readTxn([ const secretStorage = new SecretStorage({key: ssssKey, platform: this._platform, storage: this._storage});
this._storage.storeNames.accountData, const isValid = await secretStorage.hasValidKeyForAnyAccountData();
this._storage.storeNames.crossSigningKeys,
this._storage.storeNames.userIdentities,
]);
const secretStorage = new SecretStorage({key: ssssKey, platform: this._platform});
const isValid = await secretStorage.hasValidKeyForAnyAccountData(txn);
log.set("isValid", isValid); log.set("isValid", isValid);
if (isValid) { if (isValid) {
await this._loadSecretStorageServices(secretStorage, txn, log); await this._loadSecretStorageServices(secretStorage, log);
} }
return isValid; return isValid;
}); });
} }
async _loadSecretStorageServices(secretStorage, txn, log) { async _loadSecretStorageServices(secretStorage, log) {
try { try {
await log.wrap("enable key backup", async log => { await log.wrap("enable key backup", async log => {
const keyBackup = new KeyBackup( const keyBackup = new KeyBackup(
@ -345,7 +340,7 @@ export class Session {
this._storage, this._storage,
this._platform, this._platform,
); );
if (await keyBackup.load(secretStorage, txn)) { if (await keyBackup.load(secretStorage, log)) {
for (const room of this._rooms.values()) { for (const room of this._rooms.values()) {
if (room.isEncrypted) { if (room.isEncrypted) {
room.enableKeyBackup(keyBackup); room.enableKeyBackup(keyBackup);
@ -370,7 +365,7 @@ export class Session {
ownUserId: this.userId, ownUserId: this.userId,
e2eeAccount: this._e2eeAccount e2eeAccount: this._e2eeAccount
}); });
if (await crossSigning.load(txn, log)) { if (await crossSigning.load(log)) {
this._crossSigning.set(crossSigning); this._crossSigning.set(crossSigning);
} }
}); });
@ -497,15 +492,8 @@ export class Session {
olmWorker: this._olmWorker, olmWorker: this._olmWorker,
txn txn
}); });
if (this._e2eeAccount) {
log.set("keys", this._e2eeAccount.identityKeys); log.set("keys", this._e2eeAccount.identityKeys);
this._setupEncryption(); 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);
}
}
} }
const pendingEventsByRoomId = await this._getPendingEventsByRoom(txn); const pendingEventsByRoomId = await this._getPendingEventsByRoom(txn);
// load invites // load invites
@ -530,6 +518,14 @@ export class Session {
room.setInvite(invite); 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() { dispose() {
@ -570,15 +566,15 @@ export class Session {
await log.wrap("SSSSKeyFromDehydratedDeviceKey", async log => { await log.wrap("SSSSKeyFromDehydratedDeviceKey", async log => {
const ssssKey = await createSSSSKeyFromDehydratedDeviceKey(dehydratedDevice.key, this._storage, this._platform); const ssssKey = await createSSSSKeyFromDehydratedDeviceKey(dehydratedDevice.key, this._storage, this._platform);
if (ssssKey) { if (ssssKey) {
if (await this._tryLoadSecretStorage(ssssKey, undefined, log)) { if (await this._tryLoadSecretStorage(ssssKey, log)) {
log.set("success", true); log.set("success", true);
await this._writeSSSSKey(ssssKey); await this._writeSSSSKey(ssssKey);
} }
} }
}); });
} }
this._keyBackup.get()?.start(log); await this._keyBackup.get()?.start(log);
this._crossSigning.get()?.start(log); await this._crossSigning.get()?.start(log);
// restore unfinished operations, like sending out room keys // restore unfinished operations, like sending out room keys
const opsTxn = await this._storage.readWriteTxn([ const opsTxn = await this._storage.readWriteTxn([

View File

@ -163,9 +163,9 @@ export class DeviceTracker {
} }
} }
async getCrossSigningKeyForUser(userId: string, usage: KeyUsage, hsApi: HomeServerApi | undefined, existingTxn: Transaction | undefined, log: ILogItem): Promise<CrossSigningKey | undefined> { async getCrossSigningKeyForUser(userId: string, usage: KeyUsage, hsApi: HomeServerApi | undefined, log: ILogItem): Promise<CrossSigningKey | undefined> {
return await log.wrap({l: "DeviceTracker.getCrossSigningKeyForUser", id: userId, usage}, async log => { 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.userIdentities,
this._storage.storeNames.crossSigningKeys, this._storage.storeNames.crossSigningKeys,
]); ]);

View File

@ -107,9 +107,8 @@ export class KeyBackup extends EventEmitter<{change: never}> {
return txn.inboundGroupSessions.markAllAsNotBackedUp(); return txn.inboundGroupSessions.markAllAsNotBackedUp();
} }
async load(secretStorage: SecretStorage, txn: Transaction) { async load(secretStorage: SecretStorage, log: ILogItem) {
// TODO: no exception here we should anticipate? const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1");
const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1", txn);
if (base64PrivateKey) { if (base64PrivateKey) {
this.privateKey = new Uint8Array(this.platform.encoding.base64.decode(base64PrivateKey)); this.privateKey = new Uint8Array(this.platform.encoding.base64.decode(base64PrivateKey));
return true; return true;

View File

@ -40,22 +40,22 @@ class DecryptionError extends Error {
export class SecretStorage { export class SecretStorage {
private readonly _key: Key; private readonly _key: Key;
private readonly _platform: Platform; 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._key = key;
this._platform = platform; 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(); const allAccountData = await txn.accountData.getAll();
for (const accountData of allAccountData) { for (const accountData of allAccountData) {
try { 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); const secret = await this._decryptAccountData(accountData);
return true; // decryption succeeded return true; // decryption succeeded
} catch (err) { } catch (err) {
@ -69,7 +69,11 @@ export class SecretStorage {
return false; return false;
} }
async readSecret(name: string, txn: Transaction): Promise<string | undefined> { /** this method will auto-commit any indexeddb transaction because of its use of the webcrypto api */
async readSecret(name: string): Promise<string | undefined> {
const txn = await this._storage.readTxn([
this._storage.storeNames.accountData,
]);
const accountData = await txn.accountData.get(name); const accountData = await txn.accountData.get(name);
if (!accountData) { if (!accountData) {
return; return;

View File

@ -99,22 +99,22 @@ export class CrossSigning {
this.e2eeAccount = options.e2eeAccount this.e2eeAccount = options.e2eeAccount
} }
async load(txn: Transaction, log: ILogItem) { async load(log: ILogItem) {
// try to verify the msk without accessing the network // 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) { async start(log: ILogItem) {
if (!this.isMasterKeyTrusted) { if (!this.isMasterKeyTrusted) {
// try to verify the msk _with_ access to the network // 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<boolean> { private async verifyMSKFrom4S(allowNetwork: boolean, log: ILogItem): Promise<boolean> {
return await log.wrap("CrossSigning.verifyMSKFrom4S", async log => { return await log.wrap("CrossSigning.verifyMSKFrom4S", async log => {
// TODO: use errorboundary here // TODO: use errorboundary here
const privateMasterKey = await this.getSigningKey(KeyUsage.Master, txn); const privateMasterKey = await this.getSigningKey(KeyUsage.Master);
if (!privateMasterKey) { if (!privateMasterKey) {
return false; return false;
} }
@ -125,7 +125,7 @@ export class CrossSigning {
} finally { } finally {
signing.free(); 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) { if (!publishedMasterKey) {
return false; return false;
} }
@ -210,11 +210,11 @@ export class CrossSigning {
if (!this.isMasterKeyTrusted) { if (!this.isMasterKeyTrusted) {
return UserTrust.OwnSetupError; 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) { if (!ourMSK) {
return UserTrust.OwnSetupError; 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) { if (!ourUSK) {
return UserTrust.OwnSetupError; return UserTrust.OwnSetupError;
} }
@ -222,7 +222,7 @@ export class CrossSigning {
if (ourUSKVerification !== SignatureVerification.Valid) { if (ourUSKVerification !== SignatureVerification.Valid) {
return UserTrust.OwnSetupError; 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) { if (!theirMSK) {
/* assume that when they don't have an MSK, they've never enabled cross-signing on their client /* 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. (or it's not supported) rather than assuming a setup error on their side.
@ -237,7 +237,7 @@ export class CrossSigning {
return UserTrust.UserSignatureMismatch; 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) { if (!theirSSK) {
return UserTrust.UserSetupError; return UserTrust.UserSetupError;
} }
@ -290,11 +290,8 @@ export class CrossSigning {
return keyToSign; return keyToSign;
} }
private async getSigningKey(usage: KeyUsage, existingTxn?: Transaction): Promise<Uint8Array | undefined> { private async getSigningKey(usage: KeyUsage): Promise<Uint8Array | undefined> {
const txn = existingTxn ?? await this.storage.readTxn([ const seedStr = await this.secretStorage.readSecret(`m.cross_signing.${usage}`);
this.storage.storeNames.accountData,
]);
const seedStr = await this.secretStorage.readSecret(`m.cross_signing.${usage}`, txn);
if (seedStr) { if (seedStr) {
return new Uint8Array(this.platform.encoding.base64.decode(seedStr)); return new Uint8Array(this.platform.encoding.base64.decode(seedStr));
} }