Additional work to include story=true on send

Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
automated-signal 2022-10-07 12:12:27 -07:00 committed by GitHub
parent 6ff86c3747
commit 7b39315439
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 327 additions and 170 deletions

View File

@ -196,7 +196,7 @@
"@babel/preset-typescript": "7.17.12",
"@electron/fuses": "1.5.0",
"@mixer/parallel-prettier": "2.0.1",
"@signalapp/mock-server": "2.10.0",
"@signalapp/mock-server": "2.11.0",
"@storybook/addon-a11y": "6.5.6",
"@storybook/addon-actions": "6.5.6",
"@storybook/addon-controls": "6.5.6",

View File

@ -35,7 +35,8 @@ message Envelope {
optional bool ephemeral = 12; // indicates that the message should not be persisted if the recipient is offline
optional bool urgent = 14 [default=true]; // indicates that the content is considered timely by the sender; defaults to true so senders have to opt-out to say something isn't time critical
optional string updated_pni = 15;
// next: 16
optional bool story = 16; // indicates that the content is a story.
// next: 17
}
message Content {

View File

@ -131,32 +131,33 @@ message AccountRecord {
}
}
optional bytes profileKey = 1;
optional string givenName = 2;
optional string familyName = 3;
optional string avatarUrl = 4;
optional bool noteToSelfArchived = 5;
optional bool readReceipts = 6;
optional bool sealedSenderIndicators = 7;
optional bool typingIndicators = 8;
optional bool proxiedLinkPreviews = 9;
optional bool noteToSelfMarkedUnread = 10;
optional bool linkPreviews = 11;
optional PhoneNumberSharingMode phoneNumberSharingMode = 12;
optional bool notDiscoverableByPhoneNumber = 13;
repeated PinnedConversation pinnedConversations = 14;
optional bool preferContactAvatars = 15;
optional uint32 universalExpireTimer = 17;
optional bool primarySendsSms = 18;
optional string e164 = 19;
repeated string preferredReactionEmoji = 20;
optional bytes subscriberId = 21;
optional string subscriberCurrencyCode = 22;
optional bool displayBadgesOnProfile = 23;
optional bool keepMutedChatsArchived = 25;
optional bool hasSetMyStoriesPrivacy = 26;
reserved /* hasViewedOnboardingStory */ 27;
optional bool storiesDisabled = 28;
optional bytes profileKey = 1;
optional string givenName = 2;
optional string familyName = 3;
optional string avatarUrl = 4;
optional bool noteToSelfArchived = 5;
optional bool readReceipts = 6;
optional bool sealedSenderIndicators = 7;
optional bool typingIndicators = 8;
optional bool proxiedLinkPreviews = 9;
optional bool noteToSelfMarkedUnread = 10;
optional bool linkPreviews = 11;
optional PhoneNumberSharingMode phoneNumberSharingMode = 12;
optional bool notDiscoverableByPhoneNumber = 13;
repeated PinnedConversation pinnedConversations = 14;
optional bool preferContactAvatars = 15;
optional uint32 universalExpireTimer = 17;
optional bool primarySendsSms = 18;
optional string e164 = 19;
repeated string preferredReactionEmoji = 20;
optional bytes subscriberId = 21;
optional string subscriberCurrencyCode = 22;
optional bool displayBadgesOnProfile = 23;
optional bool keepMutedChatsArchived = 25;
optional bool hasSetMyStoriesPrivacy = 26;
reserved /* hasViewedOnboardingStory */ 27;
reserved 28; // deprecatedStoriesDisabled
optional bool storiesDisabled = 29;
}
message StoryDistributionListRecord {

View File

@ -183,6 +183,7 @@ export async function sendNormalMessage(
quote,
recipients: allRecipientIdentifiers,
sticker,
// No storyContext; you can't reply to your own stories
timestamp: messageTimestamp,
});
messageSendPromise = message.sendSyncMessageOnly(dataMessage, saveErrors);
@ -234,6 +235,7 @@ export async function sendNormalMessage(
sendOptions,
sendTarget: conversation.toSenderKeyTarget(),
sendType: 'message',
story: Boolean(storyContext),
urgent: true,
})
);
@ -282,6 +284,7 @@ export async function sendNormalMessage(
sticker,
storyContext,
timestamp: messageTimestamp,
// Note: 1:1 story replies should not set story=true - they aren't group sends
urgent: true,
includePniSignatureMessage: true,
});

View File

@ -264,7 +264,8 @@ export async function sendStory(
const recipientsSet = new Set(pendingSendRecipientIds);
const sendOptions = await getSendOptionsForRecipients(
pendingSendRecipientIds
pendingSendRecipientIds,
{ story: true }
);
log.info(

View File

@ -179,11 +179,13 @@ export class Reactions extends Collection<ReactionModel> {
storyReactionEmoji: reaction.get('emoji'),
});
const [generatedMessageId] = await Promise.all([
// Note: generatedMessage comes with an id, so we have to force this save
await Promise.all([
window.Signal.Data.saveMessage(generatedMessage.attributes, {
ourUuid: window.textsecure.storage.user
.getCheckedUuid()
.toString(),
forceSave: true,
}),
generatedMessage.hydrateStoryContext(message),
]);
@ -197,10 +199,8 @@ export class Reactions extends Collection<ReactionModel> {
timestamp: reaction.get('timestamp'),
});
generatedMessage.set({ id: generatedMessageId });
const messageToAdd = window.MessageController.register(
generatedMessageId,
generatedMessage.id,
generatedMessage
);
targetConversation.addSingleMessage(messageToAdd);

View File

@ -159,7 +159,6 @@ import { getMessageIdForLogging } from '../util/idForLogging';
import { hasAttachmentDownloads } from '../util/hasAttachmentDownloads';
import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads';
import { findStoryMessage } from '../util/findStoryMessage';
import { isConversationAccepted } from '../util/isConversationAccepted';
import { getStoryDataFromMessageAttributes } from '../services/storyLoader';
import type { ConversationQueueJobData } from '../jobs/conversationJobQueue';
import { getMessageById } from '../messages/getMessageById';
@ -2097,20 +2096,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
await conversation.queueJob('handleDataMessage', async () => {
log.info(`${idLog}: starting processing in queue`);
if (
isStory(message.attributes) &&
!isConversationAccepted(conversation.attributes, {
ignoreEmptyConvo: true,
})
) {
log.info(
`${idLog}: dropping story from !accepted`,
this.getSenderIdentifier()
);
confirm();
return;
}
// First, check for duplicates. If we find one, stop processing here.
const inMemoryMessage = window.MessageController.findBySender(
this.getSenderIdentifier()
@ -2387,8 +2372,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const messageId = message.get('id') || UUID.generate().toString();
// Send delivery receipts, but only for incoming sealed sender messages
// and not for messages from unaccepted conversations
// Send delivery receipts, but only for non-story sealed sender messages
// and not for messages from unaccepted conversations
if (
type === 'incoming' &&
this.get('unidentifiedDeliveryReceived') &&

View File

@ -265,6 +265,7 @@ export type UnprocessedType = {
serverTimestamp?: number;
decrypted?: string;
urgent?: boolean;
story?: boolean;
};
export type UnprocessedUpdateType = {

View File

@ -3178,6 +3178,7 @@ function saveUnprocessedSync(data: UnprocessedType): string {
serverTimestamp,
decrypted,
urgent,
story,
} = data;
if (!id) {
throw new Error('saveUnprocessedSync: id was falsey');
@ -3204,7 +3205,8 @@ function saveUnprocessedSync(data: UnprocessedType): string {
serverGuid,
serverTimestamp,
decrypted,
urgent
urgent,
story
) values (
$id,
$timestamp,
@ -3218,7 +3220,8 @@ function saveUnprocessedSync(data: UnprocessedType): string {
$serverGuid,
$serverTimestamp,
$decrypted,
$urgent
$urgent,
$story
);
`
).run({
@ -3235,6 +3238,7 @@ function saveUnprocessedSync(data: UnprocessedType): string {
serverTimestamp: serverTimestamp || null,
decrypted: decrypted || null,
urgent: urgent || !isBoolean(urgent) ? 1 : 0,
story: story ? 1 : 0,
});
return id;
@ -3309,6 +3313,7 @@ async function getUnprocessedById(
return {
...row,
urgent: isNumber(row.urgent) ? Boolean(row.urgent) : true,
story: Boolean(row.story),
};
}
@ -3370,6 +3375,7 @@ async function getAllUnprocessedAndIncrementAttempts(): Promise<
.map(row => ({
...row,
urgent: isNumber(row.urgent) ? Boolean(row.urgent) : true,
story: Boolean(row.story),
}));
})();
}

View File

@ -0,0 +1,28 @@
// Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from 'better-sqlite3';
import type { LoggerType } from '../../types/Logging';
export default function updateToSchemaVersion67(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 67) {
return;
}
db.transaction(() => {
db.exec(
`
ALTER TABLE unprocessed ADD COLUMN story INTEGER;
`
);
db.pragma('user_version = 67');
})();
logger.info('updateToSchemaVersion67: success!');
}

View File

@ -42,6 +42,7 @@ import updateToSchemaVersion63 from './63-add-urgent-to-unprocessed';
import updateToSchemaVersion64 from './64-uuid-column-for-pre-keys';
import updateToSchemaVersion65 from './65-add-storage-id-to-stickers';
import updateToSchemaVersion66 from './66-add-pni-signature-to-sent-protos';
import updateToSchemaVersion67 from './67-add-story-to-unprocessed';
function updateToSchemaVersion1(
currentVersion: number,
@ -1947,6 +1948,7 @@ export const SCHEMA_VERSIONS = [
updateToSchemaVersion64,
updateToSchemaVersion65,
updateToSchemaVersion66,
updateToSchemaVersion67,
];
export function updateSchema(db: Database, logger: LoggerType): void {

View File

@ -15,7 +15,7 @@ export const debug = createDebug('mock:test:rate-limit');
const IdentifierType = Proto.ManifestRecord.Identifier.Type;
describe('rate-limit/story', function needsName() {
describe('story/no-sender-key', function needsName() {
this.timeout(durations.MINUTE);
let bootstrap: Bootstrap;
@ -65,7 +65,7 @@ describe('rate-limit/story', function needsName() {
await bootstrap.teardown();
});
it('should request challenge and accept solution', async () => {
it('should successfully send story', async () => {
const {
server,
contactsWithoutProfileKey: contacts,
@ -115,29 +115,6 @@ describe('rate-limit/story', function needsName() {
await window.locator('button.SendStoryModal__send').click();
}
debug('Waiting for challenge');
const request = await app.waitForChallenge();
debug('Checking for presence of captcha modal');
await window
.locator('.module-Modal__title >> "Verify to continue messaging"')
.waitFor();
debug('Removing rate-limiting');
for (const contact of contacts) {
const failedMessages = server.stopRateLimiting({
source: desktop.uuid,
target: contact.device.uuid,
});
assert.isAtMost(failedMessages ?? 0, 1);
}
debug('Solving challenge');
app.solveChallenge({
seq: request.seq,
data: { captcha: 'anything' },
});
debug('Verifying that all contacts received story');
await Promise.all(
contacts.map(async contact => {

View File

@ -52,7 +52,6 @@ import { QualifiedAddress } from '../types/QualifiedAddress';
import type { UUIDStringType } from '../types/UUID';
import { UUID, UUIDKind } from '../types/UUID';
import * as Errors from '../types/errors';
import { isEnabled } from '../RemoteConfig';
import { SignalService as Proto } from '../protobuf';
import { deriveGroupFields, MASTER_KEY_LENGTH } from '../groups';
@ -115,6 +114,8 @@ import { areArraysMatchingSets } from '../util/areArraysMatchingSets';
import { generateBlurHash } from '../util/generateBlurHash';
import { TEXT_ATTACHMENT } from '../types/MIME';
import type { SendTypesType } from '../util/handleMessageSend';
import { isConversationAccepted } from '../util/isConversationAccepted';
import { getStoriesBlocked } from '../types/Stories';
const GROUPV1_ID_LENGTH = 16;
const GROUPV2_ID_LENGTH = 32;
@ -394,6 +395,7 @@ export default class MessageReceiver
serverGuid: decoded.serverGuid,
serverTimestamp,
urgent: isBoolean(decoded.urgent) ? decoded.urgent : true,
story: decoded.story,
};
// After this point, decoding errors are not the server's
@ -777,6 +779,7 @@ export default class MessageReceiver
serverTimestamp:
item.serverTimestamp || decoded.serverTimestamp?.toNumber(),
urgent: isBoolean(item.urgent) ? item.urgent : true,
story: Boolean(item.story),
};
const { decrypted } = item;
@ -1043,6 +1046,7 @@ export default class MessageReceiver
receivedAtCounter: envelope.receivedAtCounter,
timestamp: envelope.timestamp,
urgent: envelope.urgent,
story: envelope.story,
};
this.decryptAndCacheBatcher.add({
request,
@ -1271,10 +1275,10 @@ export default class MessageReceiver
envelope: UnsealedEnvelope,
uuidKind: UUIDKind
): Promise<DecryptResult> {
const logId = getEnvelopeId(envelope);
const logId = `MessageReceiver.decryptEnvelope(${getEnvelopeId(envelope)})`;
if (this.stoppingProcessing) {
log.warn(`MessageReceiver.decryptEnvelope(${logId}): dropping unsealed`);
log.warn(`${logId}: dropping unsealed`);
throw new Error('Unsealed envelope dropped due to stopping processing');
}
@ -1298,7 +1302,7 @@ export default class MessageReceiver
);
}
log.info(`MessageReceiver.decryptEnvelope(${logId})`);
log.info(logId);
const plaintext = await this.decrypt(
stores,
envelope,
@ -1307,7 +1311,7 @@ export default class MessageReceiver
);
if (!plaintext) {
log.warn('MessageReceiver.decryptEnvelope: plaintext was falsey');
log.warn(`${logId}: plaintext was falsey`);
return { plaintext, envelope };
}
@ -1331,6 +1335,53 @@ export default class MessageReceiver
envelope,
content.senderKeyDistributionMessage
);
} else {
// Note: `story = true` can be set for sender key distribution messages
const isStoryReply = Boolean(content.dataMessage?.storyContext);
const isGroupStoryReply = Boolean(
isStoryReply && content.dataMessage?.groupV2
);
const isStory = Boolean(content.storyMessage);
const isGroupStorySend = isGroupStoryReply || isStory;
const isDeleteForEveryone = Boolean(content.dataMessage?.delete);
if (envelope.story && !isGroupStorySend && !isDeleteForEveryone) {
log.warn(
`${logId}: Dropping story message - story=true on envelope, but message was not a group story send or delete`
);
this.removeFromCache(envelope);
return { plaintext: undefined, envelope };
}
if (!envelope.story && isGroupStorySend) {
log.warn(
`${logId}: Malformed story - story=false on envelope, but was a group story send`
);
}
const areStoriesBlocked = getStoriesBlocked();
// Note that there are other story-related message types which aren't captured
// here. Look for other calls to getStoriesBlocked down-file.
if (areStoriesBlocked && (isStoryReply || isStory)) {
log.warn(
`${logId}: Dropping story message - stories are disabled or unavailable`
);
this.removeFromCache(envelope);
return { plaintext: undefined, envelope };
}
const sender = window.ConversationController.get(
envelope.sourceUuid || envelope.source
);
if (
(!sender || !isConversationAccepted(sender.attributes)) &&
(isStoryReply || isStory)
) {
log.warn(`${logId}: Dropping story message - !accepted for sender`);
this.removeFromCache(envelope);
return { plaintext: undefined, envelope };
}
}
if (content.pniSignatureMessage) {
@ -1359,8 +1410,7 @@ export default class MessageReceiver
inProgressMessageType = '';
} catch (error) {
log.error(
'MessageReceiver.decryptEnvelope: ' +
`Failed to process ${inProgressMessageType} ` +
`${logId}: Failed to process ${inProgressMessageType} ` +
`message: ${Errors.toLogFormat(error)}`
);
}
@ -1371,9 +1421,7 @@ export default class MessageReceiver
((envelope.source && this.isBlocked(envelope.source)) ||
(envelope.sourceUuid && this.isUuidBlocked(envelope.sourceUuid)))
) {
log.info(
'MessageReceiver.decryptEnvelope: Dropping non-GV2 message from blocked sender'
);
log.info(`${logId}: Dropping non-GV2 message from blocked sender`);
return { plaintext: undefined, envelope };
}
@ -1900,16 +1948,19 @@ export default class MessageReceiver
sentMessage?: ProcessedSent
): Promise<void> {
const logId = getEnvelopeId(envelope);
log.info('MessageReceiver.handleStoryMessage', logId);
const attachments: Array<ProcessedAttachment> = [];
logUnexpectedUrgentValue(envelope, 'story');
if (window.Events.getHasStoriesDisabled()) {
if (getStoriesBlocked()) {
log.info('MessageReceiver.handleStoryMessage: dropping', logId);
this.removeFromCache(envelope);
return;
}
log.info('MessageReceiver.handleStoryMessage', logId);
const attachments: Array<ProcessedAttachment> = [];
if (msg.fileAttachment) {
const attachment = processAttachment(msg.fileAttachment);
attachments.push(attachment);
@ -2076,16 +2127,12 @@ export default class MessageReceiver
const logId = getEnvelopeId(envelope);
log.info('MessageReceiver.handleDataMessage', logId);
const isStoriesEnabled =
isEnabled('desktop.stories') || isEnabled('desktop.internalUser');
if (!isStoriesEnabled && msg.storyContext) {
logUnexpectedUrgentValue(envelope, 'story');
if (getStoriesBlocked() && msg.storyContext) {
log.info(
`MessageReceiver.handleDataMessage/${logId}: Dropping incoming dataMessage with storyContext field`
);
this.removeFromCache(envelope);
return undefined;
return;
}
let p: Promise<void> = Promise.resolve();
@ -2129,9 +2176,7 @@ export default class MessageReceiver
let type: SendTypesType = 'message';
if (msg.storyContext) {
type = 'story';
} else if (msg.body) {
if (msg.storyContext || msg.body) {
type = 'message';
} else if (msg.reaction) {
type = 'reaction';
@ -2294,19 +2339,8 @@ export default class MessageReceiver
return;
}
const isStoriesEnabled =
isEnabled('desktop.stories') || isEnabled('desktop.internalUser');
if (content.storyMessage) {
if (isStoriesEnabled) {
await this.handleStoryMessage(envelope, content.storyMessage);
return;
}
const logId = getEnvelopeId(envelope);
log.info(
`innerHandleContentMessage/${logId}: Dropping incoming message with storyMessage field`
);
this.removeFromCache(envelope);
await this.handleStoryMessage(envelope, content.storyMessage);
return;
}
@ -2689,16 +2723,17 @@ export default class MessageReceiver
const sentMessage = syncMessage.sent;
if (sentMessage.storyMessageRecipients && sentMessage.isRecipientUpdate) {
if (window.Events.getHasStoriesDisabled()) {
if (getStoriesBlocked()) {
log.info(
'MessageReceiver.handleSyncMessage: dropping story recipients update'
'MessageReceiver.handleSyncMessage: dropping story recipients update',
getEnvelopeId(envelope)
);
this.removeFromCache(envelope);
return;
}
log.info(
'MessageReceiver.handleSyncMessage: handling storyMessageRecipients isRecipientUpdate sync message',
'MessageReceiver.handleSyncMessage: handling story recipients update',
getEnvelopeId(envelope)
);
const ev = new StoryRecipientUpdateEvent(

View File

@ -1255,6 +1255,7 @@ export default class MessageSender {
groupId,
options,
urgent,
story,
}: Readonly<{
timestamp: number;
recipients: Array<string>;
@ -1263,6 +1264,7 @@ export default class MessageSender {
groupId: string | undefined;
options?: SendOptionsType;
urgent: boolean;
story?: boolean;
}>): Promise<CallbackResultType> {
return new Promise((resolve, reject) => {
const callback = (result: CallbackResultType) => {
@ -1282,6 +1284,7 @@ export default class MessageSender {
recipients,
timestamp,
urgent,
story,
});
});
}

View File

@ -94,6 +94,7 @@ export type ProcessedEnvelope = Readonly<{
serverTimestamp: number;
groupId?: string;
urgent?: boolean;
story?: boolean;
}>;
export type ProcessedAttachment = {

View File

@ -942,6 +942,7 @@ export type WebAPIType = {
timestamp: number,
options: {
online?: boolean;
story?: boolean;
urgent?: boolean;
}
) => Promise<MultiRecipient200ResponseType>;
@ -2122,14 +2123,13 @@ export function initialize({
messages,
timestamp,
online: Boolean(online),
story,
urgent,
};
await _ajax({
call: 'messages',
httpType: 'PUT',
urlParameters: `/${destination}`,
urlParameters: `/${destination}?story=${booleanToString(story)}`,
jsonData,
responseType: 'json',
unauthenticated: true,
@ -2151,14 +2151,13 @@ export function initialize({
messages,
timestamp,
online: Boolean(online),
story,
urgent,
};
await _ajax({
call: 'messages',
httpType: 'PUT',
urlParameters: `/${destination}`,
urlParameters: `/${destination}?story=${booleanToString(story)}`,
jsonData,
responseType: 'json',
});
@ -2175,20 +2174,23 @@ export function initialize({
{
online,
urgent = true,
story = false,
}: {
online?: boolean;
story?: boolean;
urgent?: boolean;
}
): Promise<MultiRecipient200ResponseType> {
const onlineParam = `&online=${booleanToString(online)}`;
const urgentParam = `&urgent=${booleanToString(urgent)}`;
const storyParam = `&story=${booleanToString(story)}`;
const response = await _ajax({
call: 'multiRecipient',
httpType: 'PUT',
contentType: 'application/vnd.signal-messenger.mrm',
data,
urlParameters: `?ts=${timestamp}${onlineParam}${urgentParam}`,
urlParameters: `?ts=${timestamp}${onlineParam}${urgentParam}${storyParam}`,
responseType: 'json',
unauthenticated: true,
accessKey: Bytes.toBase64(accessKeys),

View File

@ -9,6 +9,7 @@ import type { ReadStatus } from '../messages/MessageReadStatus';
import type { SendStatus } from '../messages/MessageSendState';
import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists';
import type { UUIDStringType } from './UUID';
import { isEnabled } from '../RemoteConfig';
export type ReplyType = {
author: Pick<
@ -142,3 +143,9 @@ export enum HasStories {
Read = 'Read',
Unread = 'Unread',
}
const getStoriesAvailable = () =>
isEnabled('desktop.stories') || isEnabled('desktop.internalUser');
const getStoriesDisabled = () => window.Events.getHasStoriesDisabled();
export const getStoriesBlocked = (): boolean =>
!getStoriesAvailable() || getStoriesDisabled();

View File

@ -7,7 +7,7 @@ import type {
SendOptionsType,
} from '../textsecure/SendMessage';
import * as Bytes from '../Bytes';
import { getRandomBytes } from '../Crypto';
import { getRandomBytes, getZeroes } from '../Crypto';
import { getConversationMembers } from './getConversationMembers';
import { isDirectConversation, isMe } from './whatTypeOfConversation';
import { senderCertificateService } from '../services/senderCertificate';
@ -24,14 +24,17 @@ const SEALED_SENDER = {
};
export async function getSendOptionsForRecipients(
recipients: ReadonlyArray<string>
recipients: ReadonlyArray<string>,
options?: { story?: boolean }
): Promise<SendOptionsType> {
const conversations = recipients
.map(identifier => window.ConversationController.get(identifier))
.filter(isNotNil);
const metadataList = await Promise.all(
conversations.map(conversation => getSendOptions(conversation.attributes))
conversations.map(conversation =>
getSendOptions(conversation.attributes, options)
)
);
return metadataList.reduce(
@ -58,9 +61,9 @@ export async function getSendOptionsForRecipients(
export async function getSendOptions(
conversationAttrs: ConversationAttributesType,
options: { syncMessage?: boolean } = {}
options: { syncMessage?: boolean; story?: boolean } = {}
): Promise<SendOptionsType> {
const { syncMessage } = options;
const { syncMessage, story } = options;
if (!isDirectConversation(conversationAttrs)) {
const contactCollection = getConversationMembers(conversationAttrs);
@ -97,9 +100,13 @@ export async function getSendOptions(
);
// If we've never fetched user's profile, we default to what we have
if (sealedSender === SEALED_SENDER.UNKNOWN) {
if (sealedSender === SEALED_SENDER.UNKNOWN || story) {
const identifierData = {
accessKey: accessKey || Bytes.toBase64(getRandomBytes(16)),
accessKey:
accessKey ||
(story
? Bytes.toBase64(getZeroes(16))
: Bytes.toBase64(getRandomBytes(16))),
senderCertificate,
};
return {

View File

@ -38,6 +38,7 @@ import type {
import { SignalService as Proto } from '../protobuf';
import * as log from '../logging/log';
import MessageSender from '../textsecure/SendMessage';
import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists';
const RETRY_LIMIT = 5;
@ -138,7 +139,8 @@ export async function onRetryRequest(event: RetryRequestEvent): Promise<void> {
const { contentHint, messageIds, proto, timestamp, urgent } = sentProto;
const { contentProto, groupId } = await maybeAddSenderKeyDistributionMessage({
// Only applies to sender key sends in groups. See below for story distribution lists.
const addSenderKeyResult = await maybeAddSenderKeyDistributionMessage({
contentProto: Proto.Content.decode(proto),
logId,
messageIds,
@ -146,44 +148,35 @@ export async function onRetryRequest(event: RetryRequestEvent): Promise<void> {
requesterUuid,
timestamp,
});
// eslint-disable-next-line prefer-destructuring
let contentProto: Proto.IContent | undefined =
addSenderKeyResult.contentProto;
const { groupId } = addSenderKeyResult;
// Assert that the requesting UUID is still part of a distribution list that
// the message was sent to.
if (contentProto.storyMessage) {
const { storyDistributionLists } = window.reduxStore.getState();
const membersByListId = new Map<string, Set<string>>();
storyDistributionLists.distributionLists.forEach(list => {
membersByListId.set(list.id, new Set(list.memberUuids));
// Assert that the requesting UUID is still part of a story distribution list that
// the message was sent to, and add its sender key distribution message (SKDM).
if (contentProto.storyMessage && !groupId) {
contentProto = await checkDistributionListAndAddSKDM({
confirm,
contentProto,
logId,
messaging,
requesterUuid,
timestamp,
});
const messages = await dataInterface.getMessagesBySentAt(timestamp);
const isInDistributionList = messages.some(message => {
if (!message.storyDistributionListId) {
return false;
}
const members = membersByListId.get(message.storyDistributionListId);
if (!members) {
return false;
}
return members.has(requesterUuid);
});
if (!isInDistributionList) {
log.warn(
`onRetryRequest/${logId}: requesterUuid is not in distribution list`
);
confirm();
if (!contentProto) {
return;
}
}
const story = Boolean(contentProto.storyMessage);
const recipientConversation = window.ConversationController.getOrCreate(
requesterUuid,
'private'
);
const sendOptions = await getSendOptions(recipientConversation.attributes);
const sendOptions = await getSendOptions(recipientConversation.attributes, {
story,
});
const promise = messaging.sendMessageProtoAndWait({
contentHint,
groupId,
@ -192,6 +185,7 @@ export async function onRetryRequest(event: RetryRequestEvent): Promise<void> {
recipients: [requesterUuid],
timestamp,
urgent,
story,
});
await handleMessageSend(promise, {
@ -427,6 +421,88 @@ async function getRetryConversation({
return window.ConversationController.get(conversationId);
}
async function checkDistributionListAndAddSKDM({
contentProto,
timestamp,
confirm,
logId,
requesterUuid,
messaging,
}: {
contentProto: Proto.IContent;
timestamp: number;
confirm: () => void;
requesterUuid: string;
logId: string;
messaging: MessageSender;
}): Promise<Proto.IContent | undefined> {
let distributionList: StoryDistributionListDataType | undefined;
const { storyDistributionLists } = window.reduxStore.getState();
const membersByListId = new Map<string, Set<string>>();
const listsById = new Map<string, StoryDistributionListDataType>();
storyDistributionLists.distributionLists.forEach(list => {
membersByListId.set(list.id, new Set(list.memberUuids));
listsById.set(list.id, list);
});
const messages = await dataInterface.getMessagesBySentAt(timestamp);
const isInAnyDistributionList = messages.some(message => {
const listId = message.storyDistributionListId;
if (!listId) {
return false;
}
const members = membersByListId.get(listId);
if (!members) {
return false;
}
const isInList = members.has(requesterUuid);
if (isInList) {
distributionList = listsById.get(listId);
}
return isInList;
});
if (!isInAnyDistributionList) {
log.warn(
`checkDistributionListAndAddSKDM/${logId}: requesterUuid is not in distribution list. Dropping.`
);
confirm();
return undefined;
}
strictAssert(
distributionList,
`checkDistributionListAndAddSKDM/${logId}: Should have a distribution list by this point`
);
const distributionDetails =
await window.Signal.Data.getStoryDistributionWithMembers(
distributionList.id
);
const distributionId = distributionDetails?.senderKeyInfo?.distributionId;
if (!distributionId) {
log.warn(
`onRetryRequest/${logId}: No sender key info for distribution list ${distributionList.id}`
);
return contentProto;
}
const protoWithDistributionMessage =
await messaging.getSenderKeyDistributionMessage(distributionId, {
throwIfNotInDatabase: true,
timestamp,
});
return {
...contentProto,
senderKeyDistributionMessage:
protoWithDistributionMessage.senderKeyDistributionMessage,
};
}
async function maybeAddSenderKeyDistributionMessage({
contentProto,
logId,

View File

@ -8,7 +8,7 @@ import type { UUIDStringType } from '../types/UUID';
import * as log from '../logging/log';
import dataInterface from '../sql/Client';
import { DAY, SECOND } from './durations';
import { MY_STORIES_ID } from '../types/Stories';
import { getStoriesBlocked, MY_STORIES_ID } from '../types/Stories';
import { ReadStatus } from '../messages/MessageReadStatus';
import { SeenStatus } from '../MessageSeenStatus';
import { SendStatus } from '../messages/MessageSendState';
@ -28,10 +28,17 @@ export async function sendStoryMessage(
conversationIds: Array<string>,
attachment: AttachmentType
): Promise<void> {
if (getStoriesBlocked()) {
log.warn('stories.sendStoryMessage: stories disabled, returning early');
return;
}
const { messaging } = window.textsecure;
if (!messaging) {
log.warn('stories.sendStoryMessage: messaging not available');
log.warn(
'stories.sendStoryMessage: messaging not available, returning early'
);
return;
}

View File

@ -99,6 +99,7 @@ export async function sendToGroup({
sendOptions,
sendTarget,
sendType,
story,
urgent,
}: {
abortSignal?: AbortSignal;
@ -109,6 +110,7 @@ export async function sendToGroup({
sendOptions?: SendOptionsType;
sendTarget: SenderKeyTargetType;
sendType: SendTypesType;
story?: boolean;
urgent: boolean;
}): Promise<CallbackResultType> {
strictAssert(
@ -141,6 +143,7 @@ export async function sendToGroup({
sendOptions,
sendTarget,
sendType,
story,
timestamp,
urgent,
});
@ -377,7 +380,7 @@ export async function sendToGroupViaSenderKey(options: {
// 4. Partition devices into sender key and non-sender key groups
const [devicesForSenderKey, devicesForNormalSend] = partition(
currentDevices,
device => isValidSenderKeyRecipient(memberSet, device.identifier)
device => isValidSenderKeyRecipient(memberSet, device.identifier, { story })
);
const senderKeyRecipients = getUuidsFromDevices(devicesForSenderKey);
@ -513,13 +516,13 @@ export async function sendToGroupViaSenderKey(options: {
contentMessage: Proto.Content.encode(contentMessage).finish(),
groupId,
});
const accessKeys = getXorOfAccessKeys(devicesForSenderKey);
const accessKeys = getXorOfAccessKeys(devicesForSenderKey, { story });
const result = await window.textsecure.messaging.server.sendWithSenderKey(
messageBuffer,
accessKeys,
timestamp,
{ online, urgent }
{ online, story, urgent }
);
const parsed = multiRecipient200ResponseSchema.safeParse(result);
@ -977,7 +980,10 @@ async function handle410Response(
}
}
function getXorOfAccessKeys(devices: Array<DeviceType>): Buffer {
function getXorOfAccessKeys(
devices: Array<DeviceType>,
{ story }: { story?: boolean } = {}
): Buffer {
const uuids = getUuidsFromDevices(devices);
const result = Buffer.alloc(ACCESS_KEY_LENGTH);
@ -994,7 +1000,7 @@ function getXorOfAccessKeys(devices: Array<DeviceType>): Buffer {
);
}
const accessKey = getAccessKey(conversation.attributes);
const accessKey = getAccessKey(conversation.attributes, { story });
if (!accessKey) {
throw new Error(`getXorOfAccessKeys: No accessKey for UUID ${uuid}`);
}
@ -1099,7 +1105,8 @@ async function encryptForSenderKey({
function isValidSenderKeyRecipient(
members: Set<ConversationModel>,
uuid: string
uuid: string,
{ story }: { story?: boolean } = {}
): boolean {
const memberConversation = window.ConversationController.get(uuid);
if (!memberConversation) {
@ -1121,7 +1128,7 @@ function isValidSenderKeyRecipient(
return false;
}
if (!getAccessKey(memberConversation.attributes)) {
if (!getAccessKey(memberConversation.attributes, { story })) {
return false;
}
@ -1247,10 +1254,15 @@ async function resetSenderKey(sendTarget: SenderKeyTargetType): Promise<void> {
}
function getAccessKey(
attributes: ConversationAttributesType
attributes: ConversationAttributesType,
{ story }: { story?: boolean }
): string | undefined {
const { sealedSender, accessKey } = attributes;
if (story) {
return accessKey || ZERO_ACCESS_KEY;
}
if (sealedSender === SEALED_SENDER.ENABLED) {
return accessKey || undefined;
}
@ -1307,11 +1319,13 @@ async function fetchKeysForIdentifier(
);
try {
// Note: we have no way to make an unrestricted unathenticated key fetch as part of a
// story send, so we hardcode story=false.
const { accessKeyFailed } = await getKeysForIdentifier(
identifier,
window.textsecure?.messaging?.server,
devices,
getAccessKey(emptyConversation.attributes)
getAccessKey(emptyConversation.attributes, { story: false })
);
if (accessKeyFailed) {
log.info(

View File

@ -1975,10 +1975,10 @@
node-gyp-build "^4.2.3"
uuid "^8.3.0"
"@signalapp/mock-server@2.10.0":
version "2.10.0"
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.10.0.tgz#a27246e7b912caebc0bef628303e11689bf9b74c"
integrity sha512-kHos3n8lNBhivUecEFG4g1rvYpJ6oPgzKMOsaI+vN8R1R4Pc63WXxrLsxqAI2QmAngD+nmOgbjwAvKyH4MN0+w==
"@signalapp/mock-server@2.11.0":
version "2.11.0"
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.11.0.tgz#fe5f6229c4a5c28b3591e986a1622218452c5112"
integrity sha512-m23XZ8lrBn0u+zakxkKG5SezyUg6fnWwZewFF28sHNL7fQDVPHJkFCJZgE9XJwHBDM7TYz9ca/ucReW4GIPHoQ==
dependencies:
"@signalapp/libsignal-client" "^0.20.0"
debug "^4.3.2"