Skip to content

Commit

Permalink
feat(e2ei): check all e2ei certs in a conversation against their user…
Browse files Browse the repository at this point in the history
…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
aweiss-dev and atomrc authored Mar 4, 2024
1 parent 07ab1a8 commit 4d5202c
Showing 1 changed file with 75 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 4d5202c

Please sign in to comment.