Group name spoofing warning

This commit is contained in:
Evan Hahn 2021-06-01 18:30:25 -05:00 committed by GitHub
parent 51b45ab275
commit 36c15fead4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1312 additions and 215 deletions

View file

@ -1,11 +1,12 @@
// Copyright 2019-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { pick } from 'lodash';
import { isEmpty, mapValues, pick } from 'lodash';
import React from 'react';
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import {
ContactSpoofingReviewPropType,
Timeline,
WarningType as TimelineWarningType,
} from '../../components/conversation/Timeline';
@ -14,6 +15,7 @@ import { ConversationType } from '../ducks/conversations';
import { getIntl } from '../selectors/user';
import {
getConversationByIdSelector,
getConversationMessagesSelector,
getConversationSelector,
getConversationsByTitleSelector,
@ -29,7 +31,16 @@ import { SmartTimelineLoadingRow } from './TimelineLoadingRow';
import { renderAudioAttachment } from './renderAudioAttachment';
import { renderEmojiPicker } from './renderEmojiPicker';
import { getOwn } from '../../util/getOwn';
import { assert } from '../../util/assert';
import { missingCaseError } from '../../util/missingCaseError';
import { getGroupMemberships } from '../../util/getGroupMemberships';
import {
dehydrateCollisionsWithConversations,
getCollisionsFromMemberships,
invertIdsByTitle,
} from '../../util/groupMemberNameCollisions';
import { ContactSpoofingType } from '../../util/contactSpoofing';
// Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
@ -90,54 +101,117 @@ const getWarning = (
conversation: Readonly<ConversationType>,
state: Readonly<StateType>
): undefined | TimelineWarningType => {
if (
conversation.type === 'direct' &&
!conversation.acceptedMessageRequest &&
!conversation.isBlocked
) {
const getConversationsWithTitle = getConversationsByTitleSelector(state);
const conversationsWithSameTitle = getConversationsWithTitle(
conversation.title
);
assert(
conversationsWithSameTitle.length,
'Expected at least 1 conversation with the same title (this one)'
);
switch (conversation.type) {
case 'direct':
if (!conversation.acceptedMessageRequest && !conversation.isBlocked) {
const getConversationsWithTitle = getConversationsByTitleSelector(
state
);
const conversationsWithSameTitle = getConversationsWithTitle(
conversation.title
);
assert(
conversationsWithSameTitle.length,
'Expected at least 1 conversation with the same title (this one)'
);
const safeConversation = conversationsWithSameTitle.find(
otherConversation =>
otherConversation.acceptedMessageRequest &&
otherConversation.type === 'direct' &&
otherConversation.id !== conversation.id
);
const safeConversation = conversationsWithSameTitle.find(
otherConversation =>
otherConversation.acceptedMessageRequest &&
otherConversation.type === 'direct' &&
otherConversation.id !== conversation.id
);
return safeConversation ? { safeConversation } : undefined;
if (safeConversation) {
return {
type: ContactSpoofingType.DirectConversationWithSameTitle,
safeConversation,
};
}
}
return undefined;
case 'group': {
if (conversation.left || conversation.groupVersion !== 2) {
return undefined;
}
const getConversationById = getConversationByIdSelector(state);
const { memberships } = getGroupMemberships(
conversation,
getConversationById
);
const groupNameCollisions = getCollisionsFromMemberships(memberships);
const hasGroupMembersWithSameName = !isEmpty(groupNameCollisions);
if (hasGroupMembersWithSameName) {
return {
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
acknowledgedGroupNameCollisions:
conversation.acknowledgedGroupNameCollisions || {},
groupNameCollisions: dehydrateCollisionsWithConversations(
groupNameCollisions
),
};
}
return undefined;
}
default:
throw missingCaseError(conversation.type);
}
return undefined;
};
const getContactSpoofingReview = (
selectedConversationId: string,
state: Readonly<StateType>
):
| undefined
| {
possiblyUnsafeConversation: ConversationType;
safeConversation: ConversationType;
} => {
): undefined | ContactSpoofingReviewPropType => {
const { contactSpoofingReview } = state.conversations;
if (!contactSpoofingReview) {
return undefined;
}
const conversationSelector = getConversationSelector(state);
return {
possiblyUnsafeConversation: conversationSelector(selectedConversationId),
safeConversation: conversationSelector(
contactSpoofingReview.safeConversationId
),
};
const getConversationById = getConversationByIdSelector(state);
const currentConversation = conversationSelector(selectedConversationId);
switch (contactSpoofingReview.type) {
case ContactSpoofingType.DirectConversationWithSameTitle:
return {
type: ContactSpoofingType.DirectConversationWithSameTitle,
possiblyUnsafeConversation: currentConversation,
safeConversation: conversationSelector(
contactSpoofingReview.safeConversationId
),
};
case ContactSpoofingType.MultipleGroupMembersWithSameTitle: {
const { memberships } = getGroupMemberships(
currentConversation,
getConversationById
);
const groupNameCollisions = getCollisionsFromMemberships(memberships);
const previouslyAcknowledgedTitlesById = invertIdsByTitle(
currentConversation.acknowledgedGroupNameCollisions || {}
);
const collisionInfoByTitle = mapValues(
groupNameCollisions,
conversations =>
conversations.map(conversation => ({
conversation,
oldName: getOwn(previouslyAcknowledgedTitlesById, conversation.id),
}))
);
return {
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
collisionInfoByTitle,
};
}
default:
throw missingCaseError(contactSpoofingReview);
}
};
const mapStateToProps = (state: StateType, props: ExternalProps) => {
@ -150,6 +224,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
return {
id,
...pick(conversation, [
'areWeAdmin',
'unreadCount',
'typingContact',
'isGroupV1AndDisabled',