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

@ -0,0 +1,7 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export enum ContactSpoofingType {
DirectConversationWithSameTitle,
MultipleGroupMembersWithSameTitle,
}

View file

@ -0,0 +1,68 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { mapValues, pickBy } from 'lodash';
import { groupBy, map, filter } from './iterables';
import { getOwn } from './getOwn';
import { ConversationType } from '../state/ducks/conversations';
import { isConversationNameKnown } from './isConversationNameKnown';
export type GroupNameCollisionsWithIdsByTitle = Record<string, Array<string>>;
export type GroupNameCollisionsWithConversationsByTitle = Record<
string,
Array<ConversationType>
>;
export type GroupNameCollisionsWithTitlesById = Record<string, string>;
export const dehydrateCollisionsWithConversations = (
withConversations: Readonly<GroupNameCollisionsWithConversationsByTitle>
): GroupNameCollisionsWithIdsByTitle =>
mapValues(withConversations, conversations => conversations.map(c => c.id));
export function getCollisionsFromMemberships(
memberships: Iterable<{ member: ConversationType }>
): GroupNameCollisionsWithConversationsByTitle {
const members = map(memberships, membership => membership.member);
const candidateMembers = filter(
members,
member => !member.isMe && isConversationNameKnown(member)
);
const groupedByTitle = groupBy(candidateMembers, member => member.title);
// This cast is here because `pickBy` returns a `Partial`, which is incompatible with
// `Record`. [This demonstates the problem][0], but I don't believe it's an actual
// issue in the code.
//
// Alternatively, we could filter undefined keys or something like that.
//
// [0]: https://www.typescriptlang.org/play?#code/C4TwDgpgBAYg9nKBeKAFAhgJ2AS3QGwB4AlCAYzkwBNCBnYTHAOwHMAaKJgVwFsAjCJgB8QgNwAoCk3pQAZgC5YCZFADeUABY5FAVigBfCeNCQoAISwrSFanQbN2nXgOESpMvoouYVs0UA
return (pickBy(
groupedByTitle,
group => group.length >= 2
) as unknown) as GroupNameCollisionsWithConversationsByTitle;
}
/**
* Returns `true` if the user should see a group member name collision warning, and
* `false` otherwise. Users should see these warnings if any collisions appear that they
* haven't dismissed.
*/
export const hasUnacknowledgedCollisions = (
previous: Readonly<GroupNameCollisionsWithIdsByTitle>,
current: Readonly<GroupNameCollisionsWithIdsByTitle>
): boolean =>
Object.entries(current).some(([title, currentIds]) => {
const previousIds = new Set(getOwn(previous, title) || []);
return currentIds.some(currentId => !previousIds.has(currentId));
});
export const invertIdsByTitle = (
idsByTitle: Readonly<GroupNameCollisionsWithIdsByTitle>
): GroupNameCollisionsWithTitlesById => {
const result: GroupNameCollisionsWithTitlesById = Object.create(null);
Object.entries(idsByTitle).forEach(([title, ids]) => {
ids.forEach(id => {
result[id] = title;
});
});
return result;
};

View file

@ -0,0 +1,22 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { ConversationType } from '../state/ducks/conversations';
import { missingCaseError } from './missingCaseError';
export function isConversationNameKnown(
conversation: Readonly<
Pick<ConversationType, 'e164' | 'name' | 'profileName' | 'type'>
>
): boolean {
switch (conversation.type) {
case 'direct':
return Boolean(
conversation.name || conversation.profileName || conversation.e164
);
case 'group':
return Boolean(conversation.name);
default:
throw missingCaseError(conversation.type);
}
}

View file

@ -4,6 +4,8 @@
/* eslint-disable max-classes-per-file */
/* eslint-disable no-restricted-syntax */
import { getOwn } from './getOwn';
export function isIterable(value: unknown): value is Iterable<unknown> {
return (
(typeof value === 'object' && value !== null && Symbol.iterator in value) ||
@ -88,6 +90,23 @@ class FilterIterator<T> implements Iterator<T> {
}
}
export function groupBy<T>(
iterable: Iterable<T>,
fn: (value: T) => string
): Record<string, Array<T>> {
const result: Record<string, Array<T>> = Object.create(null);
for (const value of iterable) {
const key = fn(value);
const existingGroup = getOwn(result, key);
if (existingGroup) {
existingGroup.push(value);
} else {
result[key] = [value];
}
}
return result;
}
export function map<T, ResultT>(
iterable: Iterable<T>,
fn: (value: T) => ResultT