mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-23 11:35:04 +01:00
implementing observing user trust so UI can update when signing
This commit is contained in:
parent
915ab9c683
commit
f6599708b9
@ -30,20 +30,14 @@ export class MemberDetailsViewModel extends ViewModel {
|
|||||||
this._session = options.session;
|
this._session = options.session;
|
||||||
this.track(this._powerLevelsObservable.subscribe(() => this._onPowerLevelsChange()));
|
this.track(this._powerLevelsObservable.subscribe(() => this._onPowerLevelsChange()));
|
||||||
this.track(this._observableMember.subscribe( () => this._onMemberChange()));
|
this.track(this._observableMember.subscribe( () => this._onMemberChange()));
|
||||||
this.track(this._session.crossSigning.subscribe(() => {
|
|
||||||
this.emitChange("trustShieldColor");
|
|
||||||
}));
|
|
||||||
this._userTrust = undefined;
|
this._userTrust = undefined;
|
||||||
this.init(); // TODO: call this from parent view model and do something smart with error view model if it fails async?
|
this._userTrustSubscription = undefined;
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
if (this.features.crossSigning) {
|
if (this.features.crossSigning) {
|
||||||
this._userTrust = await this.logger.run({l: "MemberDetailsViewModel.get user trust", id: this._member.userId}, log => {
|
this.track(this._session.crossSigning.subscribe(() => {
|
||||||
return this._session.crossSigning.get()?.getUserTrust(this._member.userId, log);
|
this._onCrossSigningChange();
|
||||||
});
|
}));
|
||||||
this.emitChange("trustShieldColor");
|
|
||||||
}
|
}
|
||||||
|
this._onCrossSigningChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() { return this._member.name; }
|
get name() { return this._member.name; }
|
||||||
@ -51,7 +45,8 @@ export class MemberDetailsViewModel extends ViewModel {
|
|||||||
get userId() { return this._member.userId; }
|
get userId() { return this._member.userId; }
|
||||||
|
|
||||||
get trustDescription() {
|
get trustDescription() {
|
||||||
switch (this._userTrust) {
|
switch (this._userTrust?.get()) {
|
||||||
|
case undefined: return this.i18n`Please wait…`;
|
||||||
case UserTrust.Trusted: return this.i18n`You have verified this user. This user has verified all of their sessions.`;
|
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.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.`;
|
case UserTrust.UserSignatureMismatch: return this.i18n`You appear to have signed this user, but the signature is invalid.`;
|
||||||
@ -59,18 +54,17 @@ export class MemberDetailsViewModel extends ViewModel {
|
|||||||
case UserTrust.UserDeviceSignatureMismatch: return this.i18n`This user has a session signature that is invalid.`;
|
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.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.`;
|
case UserTrust.OwnSetupError: return this.i18n`Cross-signing wasn't set up correctly on your side.`;
|
||||||
default: return this.i18n`Pending…`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get trustShieldColor() {
|
get trustShieldColor() {
|
||||||
if (!this._isEncrypted) {
|
if (!this._isEncrypted) {
|
||||||
return undefined;
|
return "";
|
||||||
}
|
}
|
||||||
switch (this._userTrust) {
|
switch (this._userTrust?.get()) {
|
||||||
case undefined:
|
case undefined:
|
||||||
case UserTrust.OwnSetupError:
|
case UserTrust.OwnSetupError:
|
||||||
return undefined;
|
return "";
|
||||||
case UserTrust.Trusted:
|
case UserTrust.Trusted:
|
||||||
return "green";
|
return "green";
|
||||||
case UserTrust.UserNotSigned:
|
case UserTrust.UserNotSigned:
|
||||||
@ -103,9 +97,10 @@ export class MemberDetailsViewModel extends ViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async signUser() {
|
async signUser() {
|
||||||
if (this._session.crossSigning) {
|
const crossSigning = this._session.crossSigning.get();
|
||||||
|
if (crossSigning) {
|
||||||
await this.logger.run("MemberDetailsViewModel.signUser", async log => {
|
await this.logger.run("MemberDetailsViewModel.signUser", async log => {
|
||||||
await this._session.crossSigning.signUser(this.userId, log);
|
await crossSigning.signUser(this.userId, log);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -150,4 +145,19 @@ export class MemberDetailsViewModel extends ViewModel {
|
|||||||
}
|
}
|
||||||
this.navigation.push("room", roomId);
|
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(trust => {
|
||||||
|
this.emitChange("trustShieldColor");
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.emitChange("trustShieldColor");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,6 +163,16 @@ export class DeviceTracker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async invalidateUserKeys(userId: string): Promise<void> {
|
||||||
|
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<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 = await this._storage.readTxn([
|
const txn = await this._storage.readTxn([
|
||||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {verifyEd25519Signature, SignatureVerification} from "../e2ee/common";
|
import {verifyEd25519Signature, SignatureVerification} from "../e2ee/common";
|
||||||
|
import {BaseObservableValue, RetainedObservableValue} from "../../observable/value";
|
||||||
import {pkSign} from "./common";
|
import {pkSign} from "./common";
|
||||||
import {SASVerification} from "./SAS/SASVerification";
|
import {SASVerification} from "./SAS/SASVerification";
|
||||||
import {ToDeviceChannel} from "./SAS/channel/Channel";
|
import {ToDeviceChannel} from "./SAS/channel/Channel";
|
||||||
@ -89,6 +90,7 @@ export class CrossSigning {
|
|||||||
private readonly e2eeAccount: Account;
|
private readonly e2eeAccount: Account;
|
||||||
private readonly deviceMessageHandler: DeviceMessageHandler;
|
private readonly deviceMessageHandler: DeviceMessageHandler;
|
||||||
private _isMasterKeyTrusted: boolean = false;
|
private _isMasterKeyTrusted: boolean = false;
|
||||||
|
private readonly observedUsers: Map<string, RetainedObservableValue<UserTrust | undefined>> = new Map();
|
||||||
private readonly deviceId: string;
|
private readonly deviceId: string;
|
||||||
private sasVerificationInProgress?: SASVerification;
|
private sasVerificationInProgress?: SASVerification;
|
||||||
public receivedSASVerifications: ObservableMap<string, SASRequest> = new ObservableMap();
|
public receivedSASVerifications: ObservableMap<string, SASRequest> = new ObservableMap();
|
||||||
@ -295,50 +297,59 @@ export class CrossSigning {
|
|||||||
};
|
};
|
||||||
const request = this.hsApi.uploadSignatures(payload, {log});
|
const request = this.hsApi.uploadSignatures(payload, {log});
|
||||||
await request.response();
|
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;
|
return keyToSign;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserTrust(userId: string, log: ILogItem): Promise<UserTrust> {
|
getUserTrust(userId: string, log: ILogItem): Promise<UserTrust> {
|
||||||
return log.wrap("getUserTrust", async log => {
|
return log.wrap("CrossSigning.getUserTrust", async log => {
|
||||||
log.set("id", userId);
|
log.set("id", userId);
|
||||||
|
const logResult = (trust: UserTrust): UserTrust => {
|
||||||
|
log.set("result", trust);
|
||||||
|
return trust;
|
||||||
|
};
|
||||||
if (!this.isMasterKeyTrusted) {
|
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));
|
const ourMSK = await log.wrap("get our msk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, log));
|
||||||
if (!ourMSK) {
|
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));
|
const ourUSK = await log.wrap("get our usk", log => this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.UserSigning, this.hsApi, log));
|
||||||
if (!ourUSK) {
|
if (!ourUSK) {
|
||||||
return UserTrust.OwnSetupError;
|
return logResult(UserTrust.OwnSetupError);
|
||||||
}
|
}
|
||||||
const ourUSKVerification = log.wrap("verify our usk", log => this.hasValidSignatureFrom(ourUSK, ourMSK, log));
|
const ourUSKVerification = log.wrap("verify our usk", log => this.hasValidSignatureFrom(ourUSK, ourMSK, log));
|
||||||
if (ourUSKVerification !== SignatureVerification.Valid) {
|
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));
|
const theirMSK = await log.wrap("get their msk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, 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.
|
||||||
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 */
|
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));
|
const theirMSKVerification = log.wrap("verify their msk", log => this.hasValidSignatureFrom(theirMSK, ourUSK, log));
|
||||||
if (theirMSKVerification !== SignatureVerification.Valid) {
|
if (theirMSKVerification !== SignatureVerification.Valid) {
|
||||||
if (theirMSKVerification === SignatureVerification.NotSigned) {
|
if (theirMSKVerification === SignatureVerification.NotSigned) {
|
||||||
return UserTrust.UserNotSigned;
|
return logResult(UserTrust.UserNotSigned);
|
||||||
} else { /* SignatureVerification.Invalid */
|
} 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));
|
const theirSSK = await log.wrap("get their ssk", log => this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.SelfSigning, this.hsApi, log));
|
||||||
if (!theirSSK) {
|
if (!theirSSK) {
|
||||||
return UserTrust.UserSetupError;
|
return logResult(UserTrust.UserSetupError);
|
||||||
}
|
}
|
||||||
const theirSSKVerification = log.wrap("verify their ssk", log => this.hasValidSignatureFrom(theirSSK, theirMSK, log));
|
const theirSSKVerification = log.wrap("verify their ssk", log => this.hasValidSignatureFrom(theirSSK, theirMSK, log));
|
||||||
if (theirSSKVerification !== SignatureVerification.Valid) {
|
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 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 => {
|
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);
|
}), SignatureVerification.Valid);
|
||||||
if (lowestDeviceVerification !== SignatureVerification.Valid) {
|
if (lowestDeviceVerification !== SignatureVerification.Valid) {
|
||||||
if (lowestDeviceVerification === SignatureVerification.NotSigned) {
|
if (lowestDeviceVerification === SignatureVerification.NotSigned) {
|
||||||
return UserTrust.UserDeviceNotSigned;
|
return logResult(UserTrust.UserDeviceNotSigned);
|
||||||
} else { /* SignatureVerification.Invalid */
|
} else { /* SignatureVerification.Invalid */
|
||||||
return UserTrust.UserDeviceSignatureMismatch;
|
return logResult(UserTrust.UserDeviceSignatureMismatch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return UserTrust.Trusted;
|
return logResult(UserTrust.Trusted);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
observeUserTrust(userId: string, log: ILogItem): BaseObservableValue<UserTrust | undefined> {
|
||||||
|
const existingValue = this.observedUsers.get(userId);
|
||||||
|
if (existingValue) {
|
||||||
|
return existingValue;
|
||||||
|
}
|
||||||
|
const observable = new RetainedObservableValue<UserTrust | undefined>(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<DeviceKey | undefined> {
|
private async signDeviceKey(keyToSign: DeviceKey, log: ILogItem): Promise<DeviceKey | undefined> {
|
||||||
const signingKey = await this.getSigningKey(KeyUsage.SelfSigning);
|
const signingKey = await this.getSigningKey(KeyUsage.SelfSigning);
|
||||||
if (!signingKey) {
|
if (!signingKey) {
|
||||||
@ -382,6 +410,11 @@ export class CrossSigning {
|
|||||||
};
|
};
|
||||||
const request = this.hsApi.uploadSignatures(payload, {log});
|
const request = this.hsApi.uploadSignatures(payload, {log});
|
||||||
await request.response();
|
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(this.ownUserId);
|
||||||
|
this.emitUserTrustUpdate(this.ownUserId, log);
|
||||||
return keyToSign;
|
return keyToSign;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -403,6 +436,16 @@ export class CrossSigning {
|
|||||||
}
|
}
|
||||||
return verifyEd25519Signature(this.olmUtil, signingKey.user_id, pubKey, pubKey, key, log);
|
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 {
|
export function getKeyUsage(keyInfo: CrossSigningKey): KeyUsage | undefined {
|
||||||
|
Loading…
Reference in New Issue
Block a user