-
Notifications
You must be signed in to change notification settings - Fork 292
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(e2ei): check all e2ei certs in a conversation against their user…
…s name and handle (WPB-6194) (#16944) * wip: verify username and handle at epoch update * feat(e2ei): match username and handle from certificate to current entity * fix: add code review changes * chore: remove not needed return value --------- Co-authored-by: Thomas Belin <[email protected]>
- Loading branch information
1 parent
07ab1a8
commit 4d5202c
Showing
1 changed file
with
75 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<string, WireIdentity[]> | 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,14 +124,68 @@ class MLSConversationVerificationStateHandler { | |
await this.checkAllConversationsVerificationState(); | ||
}; | ||
|
||
private checkUserHandle = (identity: WireIdentity, user: User): boolean => { | ||
// WireIdentity handle format is "{scheme}%40{username}@{domain}" | ||
// Example: wireapp://%[email protected] | ||
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<string, WireIdentity[]> | undefined; | ||
}> => { | ||
const userIdentities = await getAllGroupUsersIdentities(conversation.groupId); | ||
const processedUserIds: Set<string> = 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 | ||
*/ | ||
private checkAllConversationsVerificationState = async (): Promise<void> => { | ||
const conversations = this.conversationState.conversations(); | ||
await Promise.all(conversations.map(conversation => this.checkConversationVerificationState(conversation))); | ||
}; | ||
|
||
private onEpochChanged = async ({groupId}: {groupId: string}): Promise<void> => { | ||
// 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); | ||
|