mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-22 19:14:52 +01:00
Merge pull request #1068 from vector-im/cross-signing/user-trust-observable
Cross-signing: implement observing user trust so UI can update when signing
This commit is contained in:
commit
515bd24cfb
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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> {
|
||||
return await log.wrap({l: "DeviceTracker.getCrossSigningKeyForUser", id: userId, usage}, async log => {
|
||||
const txn = await this._storage.readTxn([
|
||||
|
@ -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<string, RetainedObservableValue<UserTrust | undefined>> = new Map();
|
||||
private readonly deviceId: string;
|
||||
private sasVerificationInProgress?: SASVerification;
|
||||
public receivedSASVerifications: ObservableMap<string, SASRequest> = 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<UserTrust> {
|
||||
return log.wrap("getUserTrust", async log => {
|
||||
getUserTrust(userId: string, log: ILogItem): Promise<UserTrust> {
|
||||
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<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> {
|
||||
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 {
|
||||
|
Loading…
Reference in New Issue
Block a user