diff --git a/src/script/conversation/ConversationVerificationStateHandler/MLS/MLSStateHandler.ts b/src/script/conversation/ConversationVerificationStateHandler/MLS/MLSStateHandler.ts index be2f6b6ebe2..2bc3cf62a83 100644 --- a/src/script/conversation/ConversationVerificationStateHandler/MLS/MLSStateHandler.ts +++ b/src/script/conversation/ConversationVerificationStateHandler/MLS/MLSStateHandler.ts @@ -27,18 +27,25 @@ import { getAllGroupUsersIdentities, getConversationVerificationState, MLSStatuses, + WireIdentity, } from 'src/script/E2EIdentity'; import {Conversation} from 'src/script/entity/Conversation'; +import {User} from 'src/script/entity/User'; import {E2EIVerificationMessageType} from 'src/script/message/E2EIVerificationMessageType'; import {Core} from 'src/script/service/CoreSingleton'; import {Logger, getLogger} from 'Util/Logger'; import {waitFor} from 'Util/waitFor'; -import {isMLSConversation, MLSConversation} from '../../ConversationSelectors'; +import {isMLSConversation, MLSCapableConversation, MLSConversation} from '../../ConversationSelectors'; import {ConversationState} from '../../ConversationState'; import {ConversationVerificationState} from '../../ConversationVerificationState'; import {getConversationByGroupId, OnConversationE2EIVerificationStateChange} from '../shared'; +enum UserVerificationState { + ALL_VALID = 0, + SOME_INVALID = 1, +} + class MLSConversationVerificationStateHandler { private readonly logger: Logger; @@ -64,16 +71,18 @@ class MLSConversationVerificationStateHandler { * Changes mls verification state to "degraded" * @param conversation */ - private async degradeConversation(conversation: MLSConversation) { - const userIdentities = await getAllGroupUsersIdentities(conversation.groupId); + private async degradeConversation( + conversation: MLSConversation, + userIdentities: Map | undefined, + ) { if (!userIdentities) { return; } const state = ConversationVerificationState.DEGRADED; conversation.mlsVerificationState(state); - const degradedUsers: QualifiedId[] = []; + for (const [, identities] of userIdentities.entries()) { if (identities.length > 0 && identities.some(identity => identity.status !== MLSStatuses.VALID)) { degradedUsers.push(identities[0].qualifiedUserId); @@ -115,6 +124,61 @@ class MLSConversationVerificationStateHandler { await this.checkAllConversationsVerificationState(); }; + private checkUserHandle = (identity: WireIdentity, user: User): boolean => { + // WireIdentity handle format is "{scheme}%40{username}@{domain}" + // Example: wireapp://%40hans.wurst@elna.wire.link + const {handle: identityHandle} = identity; + // We only want to check the username part of the handle + const {username, domain} = user; + return identityHandle.includes(`${username()}@${domain}`); + }; + + private checkAllUserCredentialsInConversation = async ( + conversation: MLSCapableConversation, + ): Promise<{ + userVerificationState: UserVerificationState; + userIdentities: Map | undefined; + }> => { + const userIdentities = await getAllGroupUsersIdentities(conversation.groupId); + const processedUserIds: Set = new Set(); + let userVerificationState = UserVerificationState.ALL_VALID; + + if (userIdentities) { + for (const [userId, identities] of userIdentities.entries()) { + if (processedUserIds.has(userId)) { + continue; + } + processedUserIds.add(userId); + + /** + * We need to wait for the user entity to be available + * There is a race condition when adding a new user to a conversation, the host will receive the epoch update before the user entity is available + */ + const user = await waitFor(() => conversation.allUserEntities().find(user => user.qualifiedId.id === userId)); + const identity = identities.at(0); + + if (!identity || !user) { + this.logger.warn(`Could not find user or identity for userId: ${userId}`); + userVerificationState = UserVerificationState.SOME_INVALID; + break; + } + + const matchingName = identity.displayName === user.name(); + const matchingHandle = this.checkUserHandle(identity, user); + if (!matchingHandle || !matchingName) { + this.logger.warn(`User identity and user entity do not match for userId: ${userId}`); + userVerificationState = UserVerificationState.SOME_INVALID; + break; + } + } + } + + return { + userVerificationState, + userIdentities, + }; + }; + /** * This function checks all conversations if they are verified or degraded and updates them accordingly */ @@ -122,7 +186,6 @@ class MLSConversationVerificationStateHandler { const conversations = this.conversationState.conversations(); await Promise.all(conversations.map(conversation => this.checkConversationVerificationState(conversation))); }; - private onEpochChanged = async ({groupId}: {groupId: string}): Promise => { // There could be a race condition where we would receive an epoch update for a conversation that is not yet known by the webapp. // We just wait for it to be available and then check the verification state @@ -150,14 +213,18 @@ class MLSConversationVerificationStateHandler { } const verificationState = await getConversationVerificationState(conversation.groupId); + const {userIdentities, userVerificationState} = await this.checkAllUserCredentialsInConversation(conversation); + + const isConversationStateAndAllUsersVerified = + verificationState === E2eiConversationState.Verified && userVerificationState === UserVerificationState.ALL_VALID; if ( - verificationState === E2eiConversationState.NotVerified && + !isConversationStateAndAllUsersVerified && conversation.mlsVerificationState() === ConversationVerificationState.VERIFIED ) { - return this.degradeConversation(conversation); + return this.degradeConversation(conversation, userIdentities); } else if ( - verificationState === E2eiConversationState.Verified && + isConversationStateAndAllUsersVerified && conversation.mlsVerificationState() !== ConversationVerificationState.VERIFIED ) { return this.verifyConversation(conversation);