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);
}
if (loadError) {
console.error("session load error", loadError);
console.error("session load error", loadError.stack);
}
} catch (err) {
this._error = err;

View File

@ -56,7 +56,7 @@ export class KeyBackupViewModel extends ViewModel<SegmentType, Options> {
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) {

View File

@ -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);
}
}
}
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([

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 => {
const txn = existingTxn ?? await this._storage.readTxn([
const txn = await this._storage.readTxn([
this._storage.storeNames.userIdentities,
this._storage.storeNames.crossSigningKeys,
]);

View File

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

View File

@ -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<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);
if (!accountData) {
return;

View File

@ -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<boolean> {
private async verifyMSKFrom4S(allowNetwork: boolean, log: ILogItem): Promise<boolean> {
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<Uint8Array | undefined> {
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<Uint8Array | undefined> {
const seedStr = await this.secretStorage.readSecret(`m.cross_signing.${usage}`);
if (seedStr) {
return new Uint8Array(this.platform.encoding.base64.decode(seedStr));
}