diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index b73bf4bb..3b303d04 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -30,20 +30,14 @@ export class MemberDetailsViewModel extends ViewModel { this._session = options.session; this.track(this._powerLevelsObservable.subscribe(() => this._onPowerLevelsChange())); this.track(this._observableMember.subscribe( () => this._onMemberChange())); - this.track(this._session.crossSigning.subscribe(() => { - this.emitChange("trustShieldColor"); - })); this._userTrust = undefined; - this.init(); // TODO: call this from parent view model and do something smart with error view model if it fails async? - } - - async init() { + this._userTrustSubscription = undefined; if (this.features.crossSigning) { - this._userTrust = await this.logger.run({l: "MemberDetailsViewModel.get user trust", id: this._member.userId}, log => { - return this._session.crossSigning.get()?.getUserTrust(this._member.userId, log); - }); - this.emitChange("trustShieldColor"); + this.track(this._session.crossSigning.subscribe(() => { + this._onCrossSigningChange(); + })); } + this._onCrossSigningChange(); } get name() { return this._member.name; } @@ -51,7 +45,7 @@ export class MemberDetailsViewModel extends ViewModel { get userId() { return this._member.userId; } get trustDescription() { - switch (this._userTrust) { + switch (this._userTrust?.get()) { case UserTrust.Trusted: return this.i18n`You have verified this user. This user has verified all of their sessions.`; case UserTrust.UserNotSigned: return this.i18n`You have not verified this user.`; case UserTrust.UserSignatureMismatch: return this.i18n`You appear to have signed this user, but the signature is invalid.`; @@ -59,18 +53,20 @@ export class MemberDetailsViewModel extends ViewModel { case UserTrust.UserDeviceSignatureMismatch: return this.i18n`This user has a session signature that is invalid.`; case UserTrust.UserSetupError: return this.i18n`This user hasn't set up cross-signing correctly`; case UserTrust.OwnSetupError: return this.i18n`Cross-signing wasn't set up correctly on your side.`; - default: return this.i18n`Pending…`; + case undefined: + default: // adding default as well because jslint can't check for switch exhaustiveness + return this.i18n`Please wait…`; } } get trustShieldColor() { if (!this._isEncrypted) { - return undefined; + return ""; } - switch (this._userTrust) { + switch (this._userTrust?.get()) { case undefined: case UserTrust.OwnSetupError: - return undefined; + return ""; case UserTrust.Trusted: return "green"; case UserTrust.UserNotSigned: @@ -103,9 +99,10 @@ export class MemberDetailsViewModel extends ViewModel { } async signUser() { - if (this._session.crossSigning) { + const crossSigning = this._session.crossSigning.get(); + if (crossSigning) { await this.logger.run("MemberDetailsViewModel.signUser", async log => { - await this._session.crossSigning.signUser(this.userId, log); + await crossSigning.signUser(this.userId, log); }); } } @@ -150,4 +147,19 @@ export class MemberDetailsViewModel extends ViewModel { } this.navigation.push("room", roomId); } + + _onCrossSigningChange() { + const crossSigning = this._session.crossSigning.get(); + this._userTrustSubscription = this.disposeTracked(this._userTrustSubscription); + this._userTrust = undefined; + if (crossSigning) { + this.logger.run("MemberDetailsViewModel.observeUserTrust", log => { + this._userTrust = crossSigning.observeUserTrust(this.userId, log); + this._userTrustSubscription = this.track(this._userTrust.subscribe(() => { + this.emitChange("trustShieldColor"); + })); + }); + } + this.emitChange("trustShieldColor"); + } } diff --git a/src/matrix/e2ee/DeviceTracker.ts b/src/matrix/e2ee/DeviceTracker.ts index 3a50a890..dc3e4008 100644 --- a/src/matrix/e2ee/DeviceTracker.ts +++ b/src/matrix/e2ee/DeviceTracker.ts @@ -163,6 +163,16 @@ export class DeviceTracker { } } + async invalidateUserKeys(userId: string): Promise { + const txn = await this._storage.readWriteTxn([this._storage.storeNames.userIdentities]); + const userIdentity = await txn.userIdentities.get(userId); + if (userIdentity) { + userIdentity.keysTrackingStatus = KeysTrackingStatus.Outdated; + txn.userIdentities.set(userIdentity); + } + await txn.complete(); + } + 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 = await this._storage.readTxn([ diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index d9725c9a..591d57cc 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -15,6 +15,7 @@ limitations under the License. */ import {verifyEd25519Signature, SignatureVerification} from "../e2ee/common"; +import {BaseObservableValue, RetainedObservableValue} from "../../observable/value"; import {pkSign} from "./common"; import {SASVerification} from "./SAS/SASVerification"; import {ToDeviceChannel} from "./SAS/channel/Channel"; @@ -89,6 +90,7 @@ export class CrossSigning { private readonly e2eeAccount: Account; private readonly deviceMessageHandler: DeviceMessageHandler; private _isMasterKeyTrusted: boolean = false; + private readonly observedUsers: Map> = new Map(); private readonly deviceId: string; private sasVerificationInProgress?: SASVerification; public receivedSASVerifications: ObservableMap = new ObservableMap(); @@ -295,50 +297,59 @@ export class CrossSigning { }; const request = this.hsApi.uploadSignatures(payload, {log}); await request.response(); + // we don't write the signatures to storage, as we don't want to have too many special + // cases in the trust algorithm, so instead we just clear the cross signing keys + // so that they will be refetched when trust is recalculated + await this.deviceTracker.invalidateUserKeys(userId); + this.emitUserTrustUpdate(userId, log); return keyToSign; }); } - async getUserTrust(userId: string, log: ILogItem): Promise { - return log.wrap("getUserTrust", async log => { + getUserTrust(userId: string, log: ILogItem): Promise { + return log.wrap("CrossSigning.getUserTrust", async log => { log.set("id", userId); + const logResult = (trust: UserTrust): UserTrust => { + log.set("result", trust); + return trust; + }; if (!this.isMasterKeyTrusted) { - return UserTrust.OwnSetupError; + return logResult(UserTrust.OwnSetupError); } const ourMSK = await log.wrap("get our msk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, log)); if (!ourMSK) { - return UserTrust.OwnSetupError; + return logResult(UserTrust.OwnSetupError); } const ourUSK = await log.wrap("get our usk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.UserSigning, this.hsApi, log)); if (!ourUSK) { - return UserTrust.OwnSetupError; + return logResult(UserTrust.OwnSetupError); } const ourUSKVerification = log.wrap("verify our usk", log => this.hasValidSignatureFrom(ourUSK, ourMSK, log)); if (ourUSKVerification !== SignatureVerification.Valid) { - return UserTrust.OwnSetupError; + return logResult(UserTrust.OwnSetupError); } const theirMSK = await log.wrap("get their msk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, 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. Later on, for their SSK, we _do_ assume it's a setup error as it doesn't make sense to have an MSK without a SSK */ - return UserTrust.UserNotSigned; + return logResult(UserTrust.UserNotSigned); } const theirMSKVerification = log.wrap("verify their msk", log => this.hasValidSignatureFrom(theirMSK, ourUSK, log)); if (theirMSKVerification !== SignatureVerification.Valid) { if (theirMSKVerification === SignatureVerification.NotSigned) { - return UserTrust.UserNotSigned; + return logResult(UserTrust.UserNotSigned); } else { /* SignatureVerification.Invalid */ - return UserTrust.UserSignatureMismatch; + return logResult(UserTrust.UserSignatureMismatch); } } const theirSSK = await log.wrap("get their ssk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.SelfSigning, this.hsApi, log)); if (!theirSSK) { - return UserTrust.UserSetupError; + return logResult(UserTrust.UserSetupError); } const theirSSKVerification = log.wrap("verify their ssk", log => this.hasValidSignatureFrom(theirSSK, theirMSK, log)); if (theirSSKVerification !== SignatureVerification.Valid) { - return UserTrust.UserSetupError; + return logResult(UserTrust.UserSetupError); } const theirDeviceKeys = await log.wrap("get their devices", log => this.deviceTracker.devicesForUsers([userId], this.hsApi, log)); const lowestDeviceVerification = theirDeviceKeys.reduce((lowest, dk) => log.wrap({l: "verify device", id: dk.device_id}, log => { @@ -356,15 +367,32 @@ export class CrossSigning { }), SignatureVerification.Valid); if (lowestDeviceVerification !== SignatureVerification.Valid) { if (lowestDeviceVerification === SignatureVerification.NotSigned) { - return UserTrust.UserDeviceNotSigned; + return logResult(UserTrust.UserDeviceNotSigned); } else { /* SignatureVerification.Invalid */ - return UserTrust.UserDeviceSignatureMismatch; + return logResult(UserTrust.UserDeviceSignatureMismatch); } } - return UserTrust.Trusted; + return logResult(UserTrust.Trusted); }); } + observeUserTrust(userId: string, log: ILogItem): BaseObservableValue { + const existingValue = this.observedUsers.get(userId); + if (existingValue) { + return existingValue; + } + const observable = new RetainedObservableValue(undefined, () => { + this.observedUsers.delete(userId); + }); + this.observedUsers.set(userId, observable); + log.wrapDetached("get user trust", async log => { + if (observable.get() === undefined) { + observable.set(await this.getUserTrust(userId, log)); + } + }); + return observable; + } + private async signDeviceKey(keyToSign: DeviceKey, log: ILogItem): Promise { const signingKey = await this.getSigningKey(KeyUsage.SelfSigning); if (!signingKey) { @@ -382,6 +410,11 @@ export class CrossSigning { }; const request = this.hsApi.uploadSignatures(payload, {log}); await request.response(); + // we don't write the signatures to storage, as we don't want to have too many special + // cases in the trust algorithm, so instead we just clear the device keys + // so that they will be refetched when trust is recalculated + await this.deviceTracker.invalidateUserKeys(this.ownUserId); + this.emitUserTrustUpdate(this.ownUserId, log); return keyToSign; } @@ -403,6 +436,16 @@ export class CrossSigning { } return verifyEd25519Signature(this.olmUtil, signingKey.user_id, pubKey, pubKey, key, log); } + + private emitUserTrustUpdate(userId: string, log: ILogItem) { + const observable = this.observedUsers.get(userId); + if (observable && observable.get() !== undefined) { + observable.set(undefined); + log.wrapDetached("update user trust", async log => { + observable.set(await this.getUserTrust(userId, log)); + }); + } + } } export function getKeyUsage(keyInfo: CrossSigningKey): KeyUsage | undefined {