Handle Safety Number changes while sending a story

This commit is contained in:
Josh Perez 2022-08-19 14:05:31 -04:00 committed by GitHub
parent d036803df9
commit 0fb45f045d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 392 additions and 44 deletions

View file

@ -17,7 +17,7 @@ import {
} from '@signalapp/libsignal-client';
import * as Bytes from './Bytes';
import { constantTimeEqual } from './Crypto';
import { constantTimeEqual, sha256 } from './Crypto';
import { assert, strictAssert } from './util/assert';
import { isNotNil } from './util/isNotNil';
import { Zone } from './util/Zone';
@ -1565,6 +1565,23 @@ export class SignalProtocolStore extends EventsMixin {
return undefined;
}
async getFingerprint(uuid: UUID): Promise<string | undefined> {
if (uuid === null || uuid === undefined) {
throw new Error('loadIdentityKey: uuid was undefined/null');
}
const pubKey = await this.loadIdentityKey(uuid);
if (!pubKey) {
return;
}
const hash = sha256(pubKey);
const fingerprint = hash.slice(0, 4);
return Bytes.toBase64(fingerprint);
}
private async _saveIdentityKey(data: IdentityKeyType): Promise<void> {
if (!this.identityKeys) {
throw new Error('_saveIdentityKey: this.identityKeys not yet cached!');
@ -1831,7 +1848,7 @@ export class SignalProtocolStore extends EventsMixin {
return false;
}
isUntrusted(uuid: UUID): boolean {
isUntrusted(uuid: UUID, timestampThreshold = TIMESTAMP_THRESHOLD): boolean {
if (uuid === null || uuid === undefined) {
throw new Error('isUntrusted: uuid was undefined/null');
}
@ -1842,7 +1859,7 @@ export class SignalProtocolStore extends EventsMixin {
}
if (
isMoreRecentThan(identityRecord.timestamp, TIMESTAMP_THRESHOLD) &&
isMoreRecentThan(identityRecord.timestamp, timestampThreshold) &&
!identityRecord.nonblockingApproval &&
!identityRecord.firstUse
) {

View file

@ -17,6 +17,7 @@ import { isInSystemContacts } from '../util/isInSystemContacts';
export enum SafetyNumberChangeSource {
Calling = 'Calling',
MessageSend = 'MessageSend',
Story = 'Story',
}
export type SafetyNumberProps = {

View file

@ -22,6 +22,7 @@ import { MY_STORIES_ID, getStoryDistributionListName } from '../types/Stories';
import { Modal } from './Modal';
import { StoryDistributionListName } from './StoryDistributionListName';
import { Theme } from '../util/theme';
import { isNotNil } from '../util/isNotNil';
export type PropsType = {
candidateConversations: Array<ConversationType>;
@ -36,6 +37,7 @@ export type PropsType = {
name: string,
viewerUuids: Array<UUIDStringType>
) => unknown;
onSelectedStoryList: (memberUuids: Array<string>) => unknown;
onSend: (
listIds: Array<UUIDStringType>,
conversationIds: Array<string>
@ -56,6 +58,21 @@ const Page = {
type PageType = SendStoryPage | StoriesSettingsPage;
function getListMemberUuids(
list: StoryDistributionListDataType,
signalConnections: Array<ConversationType>
): Array<string> {
if (list.id === MY_STORIES_ID && list.isBlockList) {
const excludeUuids = new Set<string>(list.memberUuids);
return signalConnections
.map(conversation => conversation.uuid)
.filter(isNotNil)
.filter(uuid => !excludeUuids.has(uuid));
}
return list.memberUuids;
}
function getListViewers(
list: StoryDistributionListDataType,
i18n: LocalizerType,
@ -85,6 +102,7 @@ export const SendStoryModal = ({
onClose,
onDistributionListCreated,
onSend,
onSelectedStoryList,
signalConnections,
tagGroupsAsNewGroupStory,
}: PropsType): JSX.Element => {
@ -300,6 +318,11 @@ export const SendStoryModal = ({
}
return new Set([...listIds]);
});
if (value) {
onSelectedStoryList(
getListMemberUuids(list, signalConnections)
);
}
}}
>
{({ id, checkboxNode }) => (
@ -352,6 +375,10 @@ export const SendStoryModal = ({
moduleClassName="SendStoryModal__distribution-list"
name="SendStoryModal__distribution-list"
onChange={(value: boolean) => {
if (!group.memberships) {
return;
}
setSelectedGroupIds(groupIds => {
if (value) {
groupIds.add(group.id);
@ -360,6 +387,9 @@ export const SendStoryModal = ({
}
return new Set([...groupIds]);
});
if (value) {
onSelectedStoryList(group.memberships.map(({ uuid }) => uuid));
}
}}
>
{({ id, checkboxNode }) => (

View file

@ -43,6 +43,7 @@ export type PropsType = {
name: string,
viewerUuids: Array<UUIDStringType>
) => unknown;
onSelectedStoryList: (memberUuids: Array<string>) => unknown;
onSend: (
listIds: Array<UUIDStringType>,
conversationIds: Array<string>,
@ -51,6 +52,7 @@ export type PropsType = {
processAttachment: (
file: File
) => Promise<void | InMemoryAttachmentDraftType>;
sendStoryModalOpenStateChanged: (isOpen: boolean) => unknown;
signalConnections: Array<ConversationType>;
tagGroupsAsNewGroupStory: (cids: Array<string>) => unknown;
} & Pick<StickerButtonProps, 'installedPacks' | 'recentStickers'>;
@ -69,9 +71,11 @@ export const StoryCreator = ({
me,
onClose,
onDistributionListCreated,
onSelectedStoryList,
onSend,
processAttachment,
recentStickers,
sendStoryModalOpenStateChanged,
signalConnections,
tagGroupsAsNewGroupStory,
}: PropsType): JSX.Element => {
@ -112,6 +116,10 @@ export const StoryCreator = ({
};
}, [file, processAttachment]);
useEffect(() => {
sendStoryModalOpenStateChanged(Boolean(draftAttachment));
}, [draftAttachment, sendStoryModalOpenStateChanged]);
return (
<>
{draftAttachment && (
@ -125,6 +133,7 @@ export const StoryCreator = ({
me={me}
onClose={() => setDraftAttachment(undefined)}
onDistributionListCreated={onDistributionListCreated}
onSelectedStoryList={onSelectedStoryList}
onSend={(listIds, groupIds) => {
onSend(listIds, groupIds, draftAttachment);
setDraftAttachment(undefined);

View file

@ -2822,19 +2822,22 @@ export class ConversationModel extends window.Backbone
return window.textsecure.storage.protocol.setApproval(uuid, true);
}
safeIsUntrusted(): boolean {
safeIsUntrusted(timestampThreshold?: number): boolean {
try {
const uuid = this.getUuid();
strictAssert(uuid, `No uuid for conversation: ${this.id}`);
return window.textsecure.storage.protocol.isUntrusted(uuid);
return window.textsecure.storage.protocol.isUntrusted(
uuid,
timestampThreshold
);
} catch (err) {
return false;
}
}
isUntrusted(): boolean {
isUntrusted(timestampThreshold?: number): boolean {
if (isDirectConversation(this.attributes)) {
return this.safeIsUntrusted();
return this.safeIsUntrusted(timestampThreshold);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (!this.contactCollection!.length) {
@ -2846,13 +2849,13 @@ export class ConversationModel extends window.Backbone
if (isMe(contact.attributes)) {
return false;
}
return contact.safeIsUntrusted();
return contact.safeIsUntrusted(timestampThreshold);
});
}
getUntrusted(): Array<ConversationModel> {
getUntrusted(timestampThreshold?: number): Array<ConversationModel> {
if (isDirectConversation(this.attributes)) {
if (this.isUntrusted()) {
if (this.isUntrusted(timestampThreshold)) {
return [this];
}
return [];
@ -2863,7 +2866,7 @@ export class ConversationModel extends window.Backbone
if (isMe(contact.attributes)) {
return false;
}
return contact.isUntrusted();
return contact.isUntrusted(timestampThreshold);
}) || []
);
}

View file

@ -12,6 +12,7 @@ import type {
GetProfileOptionsType,
GetProfileUnauthOptionsType,
} from '../textsecure/WebAPI';
import type { UUID } from '../types/UUID';
import * as log from '../logging/log';
import * as Errors from '../types/errors';
import * as Bytes from '../Bytes';
@ -359,20 +360,7 @@ async function doGetProfile(c: ConversationModel): Promise<void> {
}
if (profile.identityKey) {
const identityKey = Bytes.fromBase64(profile.identityKey);
const changed = await window.textsecure.storage.protocol.saveIdentity(
new Address(uuid, 1),
identityKey,
false
);
if (changed) {
// save identity will close all sessions except for .1, so we
// must close that one manually.
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
await window.textsecure.storage.protocol.archiveSession(
new QualifiedAddress(ourUuid, new Address(uuid, 1))
);
}
await updateIdentityKey(profile.identityKey, uuid);
}
// Update accessKey to prevent race conditions. Since we run asynchronous
@ -655,3 +643,27 @@ async function maybeGetPNICredential(
log.info('maybeGetPNICredential: updated PNI credential');
}
export async function updateIdentityKey(
identityKey: string,
uuid: UUID
): Promise<void> {
if (!identityKey) {
return;
}
const identityKeyBytes = Bytes.fromBase64(identityKey);
const changed = await window.textsecure.storage.protocol.saveIdentity(
new Address(uuid, 1),
identityKeyBytes,
false
);
if (changed) {
// save identity will close all sessions except for .1, so we
// must close that one manually.
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
await window.textsecure.storage.protocol.archiveSession(
new QualifiedAddress(ourUuid, new Address(uuid, 1))
);
}
}

View file

@ -1,10 +1,11 @@
// Copyright 2021 Signal Messenger, LLC
// Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ThunkAction, ThunkDispatch } from 'redux-thunk';
import { isEqual, noop, pick } from 'lodash';
import type { AttachmentType } from '../../types/Attachment';
import type { BodyRangeType } from '../../types/Util';
import type { ConversationModel } from '../../models/conversations';
import type { MessageAttributesType } from '../../model-types.d';
import type {
MessageChangedActionType,
@ -20,9 +21,12 @@ import * as log from '../../logging/log';
import dataInterface from '../../sql/Client';
import { DAY } from '../../util/durations';
import { ReadStatus } from '../../messages/MessageReadStatus';
import { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog';
import { StoryViewDirectionType, StoryViewModeType } from '../../types/Stories';
import { StoryRecipientUpdateEvent } from '../../textsecure/messageReceiverEvents';
import { ToastReactionFailed } from '../../components/ToastReactionFailed';
import { assert } from '../../util/assert';
import { blockSendUntilConversationsAreVerified } from '../../util/blockSendUntilConversationsAreVerified';
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
import { getMessageById } from '../../messages/getMessageById';
import { markViewed } from '../../services/MessageUpdater';
@ -46,6 +50,7 @@ import { isStory } from '../../messages/helpers';
import { onStoryRecipientUpdate } from '../../util/onStoryRecipientUpdate';
import { sendStoryMessage as doSendStoryMessage } from '../../util/sendStoryMessage';
import { useBoundActions } from '../../hooks/useBoundActions';
import { verifyStoryListMembers as doVerifyStoryListMembers } from '../../util/verifyStoryListMembers';
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
@ -78,12 +83,16 @@ export type SelectedStoryDataType = {
// State
export type StoriesStateType = {
readonly isShowingStoriesView: boolean;
readonly openedAtTimestamp: number | undefined;
readonly replyState?: {
messageId: string;
replies: Array<MessageAttributesType>;
};
readonly selectedStoryData?: SelectedStoryDataType;
readonly sendStoryModalData?: {
untrustedUuids: Array<string>;
verifiedUuids: Array<string>;
};
readonly stories: Array<StoryDataType>;
readonly storyViewMode?: StoryViewModeType;
};
@ -91,11 +100,14 @@ export type StoriesStateType = {
// Actions
const DOE_STORY = 'stories/DOE';
const LIST_MEMBERS_VERIFIED = 'stories/LIST_MEMBERS_VERIFIED';
const LOAD_STORY_REPLIES = 'stories/LOAD_STORY_REPLIES';
const MARK_STORY_READ = 'stories/MARK_STORY_READ';
const QUEUE_STORY_DOWNLOAD = 'stories/QUEUE_STORY_DOWNLOAD';
const REPLY_TO_STORY = 'stories/REPLY_TO_STORY';
export const RESOLVE_ATTACHMENT_URL = 'stories/RESOLVE_ATTACHMENT_URL';
const SEND_STORY_MODAL_OPEN_STATE_CHANGED =
'stories/SEND_STORY_MODAL_OPEN_STATE_CHANGED';
const STORY_CHANGED = 'stories/STORY_CHANGED';
const TOGGLE_VIEW = 'stories/TOGGLE_VIEW';
const VIEW_STORY = 'stories/VIEW_STORY';
@ -105,6 +117,14 @@ type DOEStoryActionType = {
payload: string;
};
type ListMembersVerified = {
type: typeof LIST_MEMBERS_VERIFIED;
payload: {
untrustedUuids: Array<string>;
verifiedUuids: Array<string>;
};
};
type LoadStoryRepliesActionType = {
type: typeof LOAD_STORY_REPLIES;
payload: {
@ -136,6 +156,11 @@ type ResolveAttachmentUrlActionType = {
};
};
type SendStoryModalOpenStateChanged = {
type: typeof SEND_STORY_MODAL_OPEN_STATE_CHANGED;
payload: number | undefined;
};
type StoryChangedActionType = {
type: typeof STORY_CHANGED;
payload: StoryDataType;
@ -157,6 +182,7 @@ type ViewStoryActionType = {
export type StoriesActionType =
| DOEStoryActionType
| ListMembersVerified
| LoadStoryRepliesActionType
| MarkStoryReadActionType
| MessageChangedActionType
@ -165,6 +191,7 @@ export type StoriesActionType =
| QueueStoryDownloadActionType
| ReplyToStoryActionType
| ResolveAttachmentUrlActionType
| SendStoryModalOpenStateChanged
| StoryChangedActionType
| ToggleViewActionType
| ViewStoryActionType;
@ -580,14 +607,52 @@ function sendStoryMessage(
listIds: Array<UUIDStringType>,
conversationIds: Array<string>,
attachment: AttachmentType
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => {
await doSendStoryMessage(listIds, conversationIds, attachment);
): ThunkAction<void, RootStateType, unknown, SendStoryModalOpenStateChanged> {
return async (dispatch, getState) => {
const { stories } = getState();
const { openedAtTimestamp, sendStoryModalData } = stories;
assert(
openedAtTimestamp,
'sendStoryMessage: openedAtTimestamp is undefined, cannot send'
);
assert(
sendStoryModalData,
'sendStoryMessage: sendStoryModalData is not defined, cannot send'
);
dispatch({
type: 'NOOP',
payload: null,
type: SEND_STORY_MODAL_OPEN_STATE_CHANGED,
payload: undefined,
});
if (sendStoryModalData.untrustedUuids.length) {
log.info('sendStoryMessage: SN changed for some conversations');
const conversationsNeedingVerification: Array<ConversationModel> =
sendStoryModalData.untrustedUuids
.map(uuid => window.ConversationController.get(uuid))
.filter(isNotNil);
if (!conversationsNeedingVerification.length) {
log.warn(
'sendStoryMessage: Could not retrieve conversations for untrusted uuids'
);
return;
}
const result = await blockSendUntilConversationsAreVerified(
conversationsNeedingVerification,
SafetyNumberChangeSource.Story,
Date.now() - openedAtTimestamp
);
if (!result) {
log.info('sendStoryMessage: did not send');
return;
}
}
await doSendStoryMessage(listIds, conversationIds, attachment);
};
}
@ -598,12 +663,69 @@ function storyChanged(story: StoryDataType): StoryChangedActionType {
};
}
function sendStoryModalOpenStateChanged(
value: boolean
): ThunkAction<void, RootStateType, unknown, SendStoryModalOpenStateChanged> {
return (dispatch, getState) => {
const { stories } = getState();
if (!stories.sendStoryModalData && value) {
dispatch({
type: SEND_STORY_MODAL_OPEN_STATE_CHANGED,
payload: Date.now(),
});
}
if (stories.sendStoryModalData && !value) {
dispatch({
type: SEND_STORY_MODAL_OPEN_STATE_CHANGED,
payload: undefined,
});
}
};
}
function toggleStoriesView(): ToggleViewActionType {
return {
type: TOGGLE_VIEW,
};
}
function verifyStoryListMembers(
memberUuids: Array<string>
): ThunkAction<void, RootStateType, unknown, ListMembersVerified> {
return async (dispatch, getState) => {
const { stories } = getState();
const { sendStoryModalData } = stories;
if (!sendStoryModalData) {
return;
}
const alreadyVerifiedUuids = new Set([...sendStoryModalData.verifiedUuids]);
const uuidsNeedingVerification = memberUuids.filter(
uuid => !alreadyVerifiedUuids.has(uuid)
);
if (!uuidsNeedingVerification.length) {
return;
}
const { untrustedUuids, verifiedUuids } = await doVerifyStoryListMembers(
uuidsNeedingVerification
);
dispatch({
type: LIST_MEMBERS_VERIFIED,
payload: {
untrustedUuids: Array.from(untrustedUuids),
verifiedUuids: Array.from(verifiedUuids),
},
});
};
}
const getSelectedStoryDataForConversationId = (
dispatch: ThunkDispatch<
RootStateType,
@ -946,8 +1068,10 @@ export const actions = {
reactToStory,
replyToStory,
sendStoryMessage,
sendStoryModalOpenStateChanged,
storyChanged,
toggleStoriesView,
verifyStoryListMembers,
viewUserStories,
viewStory,
};
@ -960,7 +1084,7 @@ export function getEmptyState(
overrideState: Partial<StoriesStateType> = {}
): StoriesStateType {
return {
isShowingStoriesView: false,
openedAtTimestamp: undefined,
stories: [],
...overrideState,
};
@ -971,15 +1095,17 @@ export function reducer(
action: Readonly<StoriesActionType>
): StoriesStateType {
if (action.type === TOGGLE_VIEW) {
const isShowingStoriesView = Boolean(state.openedAtTimestamp);
return {
...state,
isShowingStoriesView: !state.isShowingStoriesView,
selectedStoryData: state.isShowingStoriesView
openedAtTimestamp: isShowingStoriesView ? undefined : Date.now(),
replyState: undefined,
sendStoryModalData: undefined,
selectedStoryData: isShowingStoriesView
? undefined
: state.selectedStoryData,
storyViewMode: state.isShowingStoriesView
? undefined
: state.storyViewMode,
storyViewMode: isShowingStoriesView ? undefined : state.storyViewMode,
};
}
@ -1244,5 +1370,52 @@ export function reducer(
};
}
if (action.type === SEND_STORY_MODAL_OPEN_STATE_CHANGED) {
if (action.payload) {
return {
...state,
sendStoryModalData: {
untrustedUuids: [],
verifiedUuids: [],
},
};
}
return {
...state,
sendStoryModalData: undefined,
};
}
if (action.type === LIST_MEMBERS_VERIFIED) {
const sendStoryModalData = {
untrustedUuids: [],
verifiedUuids: [],
...(state.sendStoryModalData || {}),
};
const untrustedUuids = Array.from(
new Set([
...sendStoryModalData.untrustedUuids,
...action.payload.untrustedUuids,
])
);
const verifiedUuids = Array.from(
new Set([
...sendStoryModalData.verifiedUuids,
...action.payload.verifiedUuids,
])
);
return {
...state,
sendStoryModalData: {
...sendStoryModalData,
untrustedUuids,
verifiedUuids,
},
};
}
return state;
}

View file

@ -37,7 +37,7 @@ export const getStoriesState = (state: StateType): StoriesStateType =>
export const shouldShowStoriesView = createSelector(
getStoriesState,
({ isShowingStoriesView }): boolean => isShowingStoriesView
({ openedAtTimestamp }): boolean => Boolean(openedAtTimestamp)
);
export const hasSelectedStoryData = createSelector(

View file

@ -13,7 +13,7 @@ import { getMe } from '../selectors/conversations';
import { getIntl } from '../selectors/user';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { getPreferredLeftPaneWidth } from '../selectors/items';
import { getStories } from '../selectors/stories';
import { getStories, shouldShowStoriesView } from '../selectors/stories';
import { saveAttachment } from '../../util/saveAttachment';
import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals';
@ -37,7 +37,7 @@ export function SmartStories(): JSX.Element | null {
const i18n = useSelector<StateType, LocalizerType>(getIntl);
const isShowingStoriesView = useSelector<StateType, boolean>(
(state: StateType) => state.stories.isShowingStoriesView
shouldShowStoriesView
);
const preferredWidthFromStorage = useSelector<StateType, number>(

View file

@ -39,7 +39,11 @@ export function SmartStoryCreator({
onClose,
}: PropsType): JSX.Element | null {
const { debouncedMaybeGrabLinkPreview } = useLinkPreviewActions();
const { sendStoryMessage } = useStoriesActions();
const {
sendStoryModalOpenStateChanged,
sendStoryMessage,
verifyStoryListMembers,
} = useStoriesActions();
const { tagGroupsAsNewGroupStory } = useConversationsActions();
const { createDistributionList } = useStoryDistributionListsActions();
@ -70,9 +74,11 @@ export function SmartStoryCreator({
me={me}
onClose={onClose}
onDistributionListCreated={createDistributionList}
onSelectedStoryList={verifyStoryListMembers}
onSend={sendStoryMessage}
processAttachment={processAttachment}
recentStickers={recentStickers}
sendStoryModalOpenStateChanged={sendStoryModalOpenStateChanged}
signalConnections={signalConnections}
tagGroupsAsNewGroupStory={tagGroupsAsNewGroupStory}
/>

View file

@ -486,6 +486,7 @@ const URL_CALLS = {
accountExistence: 'v1/accounts/account',
attachmentId: 'v2/attachments/form/upload',
attestation: 'v1/attestation',
batchIdentityCheck: 'v1/profile/identity_check/batch',
boostBadges: 'v1/subscription/boost/badges',
challenge: 'v1/challenge',
config: 'v1/config',
@ -782,6 +783,20 @@ export type GetGroupCredentialsResultType = Readonly<{
credentials: ReadonlyArray<GroupCredentialType>;
}>;
const verifyAciResponse = z
.object({
elements: z.array(
z.object({
aci: z.string(),
identityKey: z.string(),
})
),
})
.passthrough();
export type VerifyAciRequestType = Array<{ aci: string; fingerprint: string }>;
export type VerifyAciResponseType = z.infer<typeof verifyAciResponse>;
export type WebAPIType = {
startRegistration(): unknown;
finishRegistration(baton: unknown): void;
@ -878,6 +893,9 @@ export type WebAPIType = {
inviteLinkBase64?: string
) => Promise<Proto.IGroupChange>;
modifyStorageRecords: MessageSender['modifyStorageRecords'];
postBatchIdentityCheck: (
elements: VerifyAciRequestType
) => Promise<VerifyAciResponseType>;
putAttachment: (encryptedBin: Uint8Array) => Promise<string>;
putProfile: (
jsonData: ProfileRequestDataType
@ -1272,6 +1290,7 @@ export function initialize({
makeSfuRequest,
modifyGroup,
modifyStorageRecords,
postBatchIdentityCheck,
putAttachment,
putProfile,
putStickers,
@ -1559,6 +1578,28 @@ export function initialize({
});
}
async function postBatchIdentityCheck(elements: VerifyAciRequestType) {
const res = await _ajax({
data: JSON.stringify({ elements }),
call: 'batchIdentityCheck',
httpType: 'POST',
responseType: 'json',
});
const result = verifyAciResponse.safeParse(res);
if (result.success) {
return result.data;
}
log.warn(
'WebAPI: invalid response from postBatchIdentityCheck',
toLogFormat(result.error)
);
throw result.error;
}
function getProfileUrl(
identifier: string,
{

View file

@ -9,7 +9,8 @@ import { getConversationIdForLogging } from './idForLogging';
export async function blockSendUntilConversationsAreVerified(
conversations: Array<ConversationModel>,
source?: SafetyNumberChangeSource
source?: SafetyNumberChangeSource,
timestampThreshold?: number
): Promise<boolean> {
const conversationsToPause = new Map<string, Set<string>>();
@ -33,7 +34,7 @@ export async function blockSendUntilConversationsAreVerified(
});
}
const untrusted = conversation.getUntrusted();
const untrusted = conversation.getUntrusted(timestampThreshold);
if (untrusted.length) {
untrusted.forEach(untrustedConversation => {
const uuid = untrustedConversation.get('uuid');

View file

@ -0,0 +1,55 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { UUID } from '../types/UUID';
import * as log from '../logging/log';
import { isNotNil } from './isNotNil';
import { updateIdentityKey } from '../services/profiles';
export async function verifyStoryListMembers(
uuids: Array<string>
): Promise<{ untrustedUuids: Set<string>; verifiedUuids: Set<string> }> {
const { server } = window.textsecure;
if (!server) {
throw new Error('verifyStoryListMembers: server not available');
}
const verifiedUuids = new Set<string>();
const untrustedUuids = new Set<string>();
const elements = await Promise.all(
uuids.map(async aci => {
const uuid = new UUID(aci);
const fingerprint =
await window.textsecure.storage.protocol.getFingerprint(uuid);
if (!fingerprint) {
log.warn('verifyStoryListMembers: no fingerprint found for uuid=', aci);
untrustedUuids.add(aci);
return;
}
verifiedUuids.add(aci);
return { aci, fingerprint };
})
);
const { elements: unverifiedACI } = await server.postBatchIdentityCheck(
elements.filter(isNotNil)
);
await Promise.all(
unverifiedACI.map(async ({ aci, identityKey }) => {
untrustedUuids.add(aci);
verifiedUuids.delete(aci);
if (identityKey) {
await updateIdentityKey(identityKey, new UUID(aci));
} else {
await window.ConversationController.get(aci)?.getProfiles();
}
})
);
return { untrustedUuids, verifiedUuids };
}