Refactor sender key sends to allow distribution lists

This commit is contained in:
Scott Nonnenberg 2021-12-09 18:15:59 -08:00 committed by GitHub
parent 61a6f1b4cf
commit 161b0e5379
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 210 additions and 110 deletions

View file

@ -1316,10 +1316,10 @@ export async function modifyGroupV2({
timestamp, timestamp,
profileKey, profileKey,
}, },
conversation,
contentHint: ContentHint.RESENDABLE, contentHint: ContentHint.RESENDABLE,
messageId: undefined, messageId: undefined,
sendOptions, sendOptions,
sendTarget: conversation.toSenderKeyTarget(),
sendType: 'groupChange', sendType: 'groupChange',
}), }),
{ messageIds: [], sendType: 'groupChange' } { messageIds: [], sendType: 'groupChange' }
@ -1686,15 +1686,15 @@ export async function createGroupV2({
messageIds: [], messageIds: [],
send: async () => send: async () =>
window.Signal.Util.sendToGroup({ window.Signal.Util.sendToGroup({
contentHint: ContentHint.RESENDABLE,
groupSendOptions: { groupSendOptions: {
groupV2: groupV2Info, groupV2: groupV2Info,
timestamp, timestamp,
profileKey, profileKey,
}, },
conversation,
contentHint: ContentHint.RESENDABLE,
messageId: undefined, messageId: undefined,
sendOptions, sendOptions,
sendTarget: conversation.toSenderKeyTarget(),
sendType: 'groupChange', sendType: 'groupChange',
}), }),
sendType: 'groupChange', sendType: 'groupChange',
@ -2213,6 +2213,7 @@ export async function initiateMigrationToGroupV2(
send: async () => send: async () =>
// Minimal message to notify group members about migration // Minimal message to notify group members about migration
window.Signal.Util.sendToGroup({ window.Signal.Util.sendToGroup({
contentHint: ContentHint.RESENDABLE,
groupSendOptions: { groupSendOptions: {
groupV2: conversation.getGroupV2Info({ groupV2: conversation.getGroupV2Info({
includePendingMembers: true, includePendingMembers: true,
@ -2220,10 +2221,9 @@ export async function initiateMigrationToGroupV2(
timestamp, timestamp,
profileKey: ourProfileKey, profileKey: ourProfileKey,
}, },
conversation,
contentHint: ContentHint.RESENDABLE,
messageId: undefined, messageId: undefined,
sendOptions, sendOptions,
sendTarget: conversation.toSenderKeyTarget(),
sendType: 'groupChange', sendType: 'groupChange',
}), }),
sendType: 'groupChange', sendType: 'groupChange',

View file

@ -214,6 +214,7 @@ export class NormalMessageSendJobQueue extends JobQueue<NormalMessageSendJobData
'normalMessageSendJobQueue', 'normalMessageSendJobQueue',
() => () =>
window.Signal.Util.sendToGroup({ window.Signal.Util.sendToGroup({
contentHint: ContentHint.RESENDABLE,
groupSendOptions: { groupSendOptions: {
attachments, attachments,
deletedForEveryoneTimestamp, deletedForEveryoneTimestamp,
@ -232,10 +233,9 @@ export class NormalMessageSendJobQueue extends JobQueue<NormalMessageSendJobData
timestamp: messageTimestamp, timestamp: messageTimestamp,
mentions, mentions,
}, },
conversation,
contentHint: ContentHint.RESENDABLE,
messageId, messageId,
sendOptions, sendOptions,
sendTarget: conversation.toSenderKeyTarget(),
sendType: 'message', sendType: 'message',
}) })
); );

View file

@ -201,6 +201,7 @@ export class ReactionJobQueue extends JobQueue<ReactionJobData> {
} else { } else {
log.info('sending group reaction message'); log.info('sending group reaction message');
promise = window.Signal.Util.sendToGroup({ promise = window.Signal.Util.sendToGroup({
contentHint: ContentHint.RESENDABLE,
groupSendOptions: { groupSendOptions: {
groupV1: conversation.getGroupV1Info( groupV1: conversation.getGroupV1Info(
recipientIdentifiersWithoutMe recipientIdentifiersWithoutMe
@ -213,10 +214,9 @@ export class ReactionJobQueue extends JobQueue<ReactionJobData> {
expireTimer, expireTimer,
profileKey, profileKey,
}, },
conversation,
contentHint: ContentHint.RESENDABLE,
messageId, messageId,
sendOptions, sendOptions,
sendTarget: conversation.toSenderKeyTarget(),
sendType: 'reaction', sendType: 'reaction',
}); });
} }

View file

@ -11,6 +11,7 @@ import type {
MessageAttributesType, MessageAttributesType,
MessageModelCollectionType, MessageModelCollectionType,
QuotedMessageType, QuotedMessageType,
SenderKeyInfoType,
VerificationOptions, VerificationOptions,
WhatIsThis, WhatIsThis,
} from '../model-types.d'; } from '../model-types.d';
@ -102,6 +103,7 @@ import { createIdenticon } from '../util/createIdenticon';
import * as log from '../logging/log'; import * as log from '../logging/log';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
import { isMessageUnread } from '../util/isMessageUnread'; import { isMessageUnread } from '../util/isMessageUnread';
import type { SenderKeyTargetType } from '../util/sendToGroup';
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -356,6 +358,23 @@ export class ConversationModel extends window.Backbone
} }
} }
toSenderKeyTarget(): SenderKeyTargetType {
return {
getGroupId: () => this.get('groupId'),
getMembers: () => this.getMembers(),
hasMember: (id: string) => this.hasMember(id),
idForLogging: () => this.idForLogging(),
isGroupV2: () => isGroupV2(this.attributes),
isValid: () => isGroupV2(this.attributes),
getSenderKeyInfo: () => this.get('senderKeyInfo'),
saveSenderKeyInfo: async (senderKeyInfo: SenderKeyInfoType) => {
this.set({ senderKeyInfo });
window.Signal.Data.updateConversation(this.attributes);
},
};
}
isMemberRequestingToJoin(id: string): boolean { isMemberRequestingToJoin(id: string): boolean {
if (!isGroupV2(this.attributes)) { if (!isGroupV2(this.attributes)) {
return false; return false;
@ -1272,11 +1291,11 @@ export class ConversationModel extends window.Backbone
window.Signal.Util.sendContentMessageToGroup({ window.Signal.Util.sendContentMessageToGroup({
contentHint: ContentHint.IMPLICIT, contentHint: ContentHint.IMPLICIT,
contentMessage, contentMessage,
conversation: this,
messageId: undefined, messageId: undefined,
online: true, online: true,
recipients: groupMembers, recipients: groupMembers,
sendOptions, sendOptions,
sendTarget: this.toSenderKeyTarget(),
sendType: 'typing', sendType: 'typing',
timestamp, timestamp,
}), }),
@ -3759,6 +3778,7 @@ export class ConversationModel extends window.Backbone
} }
return window.Signal.Util.sendToGroup({ return window.Signal.Util.sendToGroup({
contentHint: ContentHint.RESENDABLE,
groupSendOptions: { groupSendOptions: {
groupV1: this.getGroupV1Info(), groupV1: this.getGroupV1Info(),
groupV2: this.getGroupV2Info(), groupV2: this.getGroupV2Info(),
@ -3766,10 +3786,9 @@ export class ConversationModel extends window.Backbone
timestamp, timestamp,
profileKey, profileKey,
}, },
conversation: this,
contentHint: ContentHint.RESENDABLE,
messageId, messageId,
sendOptions, sendOptions,
sendTarget: this.toSenderKeyTarget(),
sendType: 'deleteForEveryone', sendType: 'deleteForEveryone',
}); });
})(); })();

View file

@ -949,15 +949,15 @@ export class CallingClass {
send: () => send: () =>
conversation.queueJob('sendGroupCallUpdateMessage', () => conversation.queueJob('sendGroupCallUpdateMessage', () =>
window.Signal.Util.sendToGroup({ window.Signal.Util.sendToGroup({
contentHint: ContentHint.DEFAULT,
groupSendOptions: { groupSendOptions: {
groupCallUpdate: { eraId }, groupCallUpdate: { eraId },
groupV2, groupV2,
timestamp, timestamp,
}, },
conversation,
contentHint: ContentHint.DEFAULT,
messageId: undefined, messageId: undefined,
sendOptions, sendOptions,
sendTarget: conversation.toSenderKeyTarget(),
sendType: 'callingMessage', sendType: 'callingMessage',
}) })
), ),
@ -1567,11 +1567,11 @@ export class CallingClass {
window.Signal.Util.sendContentMessageToGroup({ window.Signal.Util.sendContentMessageToGroup({
contentHint: ContentHint.DEFAULT, contentHint: ContentHint.DEFAULT,
contentMessage, contentMessage,
conversation,
isPartialSend: false, isPartialSend: false,
messageId: undefined, messageId: undefined,
recipients: conversation.getRecipients(), recipients: conversation.getRecipients(),
sendOptions: await getSendOptions(conversation.attributes), sendOptions: await getSendOptions(conversation.attributes),
sendTarget: conversation.toSenderKeyTarget(),
sendType: 'callingMessage', sendType: 'callingMessage',
timestamp, timestamp,
}), }),

View file

@ -288,6 +288,7 @@ const dataInterface: ClientInterface = {
_deleteAllStoryDistributions, _deleteAllStoryDistributions,
createNewStoryDistribution, createNewStoryDistribution,
getAllStoryDistributionsWithMembers, getAllStoryDistributionsWithMembers,
modifyStoryDistribution,
modifyStoryDistributionMembers, modifyStoryDistributionMembers,
deleteStoryDistribution, deleteStoryDistribution,
@ -1660,15 +1661,20 @@ async function _deleteAllStoryDistributions(): Promise<void> {
await channels._deleteAllStoryDistributions(); await channels._deleteAllStoryDistributions();
} }
async function createNewStoryDistribution( async function createNewStoryDistribution(
story: StoryDistributionWithMembersType distribution: StoryDistributionWithMembersType
): Promise<void> { ): Promise<void> {
await channels.createNewStoryDistribution(story); await channels.createNewStoryDistribution(distribution);
} }
async function getAllStoryDistributionsWithMembers(): Promise< async function getAllStoryDistributionsWithMembers(): Promise<
Array<StoryDistributionWithMembersType> Array<StoryDistributionWithMembersType>
> { > {
return channels.getAllStoryDistributionsWithMembers(); return channels.getAllStoryDistributionsWithMembers();
} }
async function modifyStoryDistribution(
distribution: StoryDistributionType
): Promise<void> {
await channels.modifyStoryDistribution(distribution);
}
async function modifyStoryDistributionMembers( async function modifyStoryDistributionMembers(
id: string, id: string,
options: { options: {

View file

@ -499,11 +499,12 @@ export type DataInterface = {
>; >;
_deleteAllStoryDistributions(): Promise<void>; _deleteAllStoryDistributions(): Promise<void>;
createNewStoryDistribution( createNewStoryDistribution(
story: StoryDistributionWithMembersType distribution: StoryDistributionWithMembersType
): Promise<void>; ): Promise<void>;
getAllStoryDistributionsWithMembers(): Promise< getAllStoryDistributionsWithMembers(): Promise<
Array<StoryDistributionWithMembersType> Array<StoryDistributionWithMembersType>
>; >;
modifyStoryDistribution(distribution: StoryDistributionType): Promise<void>;
modifyStoryDistributionMembers( modifyStoryDistributionMembers(
id: string, id: string,
options: { options: {

View file

@ -280,6 +280,7 @@ const dataInterface: ServerInterface = {
_deleteAllStoryDistributions, _deleteAllStoryDistributions,
createNewStoryDistribution, createNewStoryDistribution,
getAllStoryDistributionsWithMembers, getAllStoryDistributionsWithMembers,
modifyStoryDistribution,
modifyStoryDistributionMembers, modifyStoryDistributionMembers,
deleteStoryDistribution, deleteStoryDistribution,
@ -3848,12 +3849,12 @@ async function _deleteAllStoryDistributions(): Promise<void> {
db.prepare<EmptyQuery>('DELETE FROM storyDistributions;').run(); db.prepare<EmptyQuery>('DELETE FROM storyDistributions;').run();
} }
async function createNewStoryDistribution( async function createNewStoryDistribution(
story: StoryDistributionWithMembersType distribution: StoryDistributionWithMembersType
): Promise<void> { ): Promise<void> {
const db = getInstance(); const db = getInstance();
db.transaction(() => { db.transaction(() => {
const payload = freezeStoryDistribution(story); const payload = freezeStoryDistribution(distribution);
prepare( prepare(
db, db,
@ -3874,7 +3875,7 @@ async function createNewStoryDistribution(
` `
).run(payload); ).run(payload);
const { id: listId, members } = story; const { id: listId, members } = distribution;
const memberInsertStatement = prepare( const memberInsertStatement = prepare(
db, db,
@ -3910,6 +3911,24 @@ async function getAllStoryDistributionsWithMembers(): Promise<
members: (byListId[list.id] || []).map(member => member.uuid), members: (byListId[list.id] || []).map(member => member.uuid),
})); }));
} }
async function modifyStoryDistribution(
distribution: StoryDistributionType
): Promise<void> {
const payload = freezeStoryDistribution(distribution);
const db = getInstance();
prepare(
db,
`
UPDATE storyDistributions
SET
name = $name,
avatarUrlPath = $avatarUrlPath,
avatarKey = $avatarKey,
senderKeyInfoJson = $senderKeyInfoJson
WHERE id = $id
`
).run(payload);
}
async function modifyStoryDistributionMembers( async function modifyStoryDistributionMembers(
listId: string, listId: string,
{ {

View file

@ -17,6 +17,7 @@ const {
createNewStoryDistribution, createNewStoryDistribution,
deleteStoryDistribution, deleteStoryDistribution,
getAllStoryDistributionsWithMembers, getAllStoryDistributionsWithMembers,
modifyStoryDistribution,
modifyStoryDistributionMembers, modifyStoryDistributionMembers,
} = dataInterface; } = dataInterface;
@ -59,6 +60,55 @@ describe('sql/storyDistribution', () => {
assert.lengthOf(await getAllStoryDistributionsWithMembers(), 0); assert.lengthOf(await getAllStoryDistributionsWithMembers(), 0);
}); });
it('updates core fields with modifyStoryDistribution', async () => {
const UUID_1 = getUuid();
const UUID_2 = getUuid();
const list: StoryDistributionWithMembersType = {
id: getUuid(),
name: 'My Story',
avatarUrlPath: getUuid(),
avatarKey: getRandomBytes(128),
members: [UUID_1, UUID_2],
senderKeyInfo: {
createdAtDate: Date.now(),
distributionId: getUuid(),
memberDevices: [],
},
};
await createNewStoryDistribution(list);
assert.lengthOf(await _getAllStoryDistributions(), 1);
assert.lengthOf(await _getAllStoryDistributionMembers(), 2);
const updated = {
...list,
name: 'Updated story',
avatarKey: getRandomBytes(128),
avatarUrlPath: getUuid(),
senderKeyInfo: {
createdAtDate: Date.now() + 10,
distributionId: getUuid(),
memberDevices: [
{
id: 1,
identifier: UUID_1,
registrationId: 232,
},
],
},
};
await modifyStoryDistribution(updated);
assert.lengthOf(await _getAllStoryDistributions(), 1);
assert.lengthOf(await _getAllStoryDistributionMembers(), 2);
const allHydratedLists = await getAllStoryDistributionsWithMembers();
assert.lengthOf(allHydratedLists, 1);
assert.deepEqual(allHydratedLists[0], updated);
});
it('adds and removes with modifyStoryDistributionMembers', async () => { it('adds and removes with modifyStoryDistributionMembers', async () => {
const UUID_1 = getUuid(); const UUID_1 = getUuid();
const UUID_2 = getUuid(); const UUID_2 = getUuid();

View file

@ -34,7 +34,10 @@ import { IdentityKeys, SenderKeys, Sessions } from '../LibSignalStores';
import type { ConversationModel } from '../models/conversations'; import type { ConversationModel } from '../models/conversations';
import type { DeviceType, CallbackResultType } from '../textsecure/Types.d'; import type { DeviceType, CallbackResultType } from '../textsecure/Types.d';
import { getKeysForIdentifier } from '../textsecure/getKeysForIdentifier'; import { getKeysForIdentifier } from '../textsecure/getKeysForIdentifier';
import type { ConversationAttributesType } from '../model-types.d'; import type {
ConversationAttributesType,
SenderKeyInfoType,
} from '../model-types.d';
import type { SendTypesType } from './handleMessageSend'; import type { SendTypesType } from './handleMessageSend';
import { import {
handleMessageSend, handleMessageSend,
@ -51,7 +54,6 @@ import { SignalService as Proto } from '../protobuf';
import * as RemoteConfig from '../RemoteConfig'; import * as RemoteConfig from '../RemoteConfig';
import { strictAssert } from './assert'; import { strictAssert } from './assert';
import { isGroupV2 } from './whatTypeOfConversation';
import * as log from '../logging/log'; import * as log from '../logging/log';
const ERROR_EXPIRED_OR_MISSING_DEVICES = 409; const ERROR_EXPIRED_OR_MISSING_DEVICES = 409;
@ -70,21 +72,33 @@ const ZERO_ACCESS_KEY = Bytes.toBase64(new Uint8Array(ACCESS_KEY_LENGTH));
// Public API: // Public API:
export type SenderKeyTargetType = {
getGroupId: () => string | undefined;
getMembers: () => Array<ConversationModel>;
hasMember: (id: string) => boolean;
idForLogging: () => string;
isGroupV2: () => boolean;
isValid: () => boolean;
getSenderKeyInfo: () => SenderKeyInfoType | undefined;
saveSenderKeyInfo: (senderKeyInfo: SenderKeyInfoType) => Promise<void>;
};
export async function sendToGroup({ export async function sendToGroup({
contentHint, contentHint,
conversation,
groupSendOptions, groupSendOptions,
messageId,
isPartialSend, isPartialSend,
messageId,
sendOptions, sendOptions,
sendTarget,
sendType, sendType,
}: { }: {
contentHint: number; contentHint: number;
conversation: ConversationModel;
groupSendOptions: GroupSendOptionsType; groupSendOptions: GroupSendOptionsType;
isPartialSend?: boolean; isPartialSend?: boolean;
messageId: string | undefined; messageId: string | undefined;
sendOptions?: SendOptionsType; sendOptions?: SendOptionsType;
sendTarget: SenderKeyTargetType;
sendType: SendTypesType; sendType: SendTypesType;
}): Promise<CallbackResultType> { }): Promise<CallbackResultType> {
strictAssert( strictAssert(
@ -105,11 +119,11 @@ export async function sendToGroup({
return sendContentMessageToGroup({ return sendContentMessageToGroup({
contentHint, contentHint,
contentMessage, contentMessage,
conversation,
isPartialSend, isPartialSend,
messageId, messageId,
recipients, recipients,
sendOptions, sendOptions,
sendTarget,
sendType, sendType,
timestamp, timestamp,
}); });
@ -118,27 +132,27 @@ export async function sendToGroup({
export async function sendContentMessageToGroup({ export async function sendContentMessageToGroup({
contentHint, contentHint,
contentMessage, contentMessage,
conversation,
isPartialSend, isPartialSend,
messageId, messageId,
online, online,
recipients, recipients,
sendOptions, sendOptions,
sendTarget,
sendType, sendType,
timestamp, timestamp,
}: { }: {
contentHint: number; contentHint: number;
contentMessage: Proto.Content; contentMessage: Proto.Content;
conversation: ConversationModel;
isPartialSend?: boolean; isPartialSend?: boolean;
messageId: string | undefined; messageId: string | undefined;
online?: boolean; online?: boolean;
recipients: Array<string>; recipients: Array<string>;
sendOptions?: SendOptionsType; sendOptions?: SendOptionsType;
sendTarget: SenderKeyTargetType;
sendType: SendTypesType; sendType: SendTypesType;
timestamp: number; timestamp: number;
}): Promise<CallbackResultType> { }): Promise<CallbackResultType> {
const logId = conversation.idForLogging(); const logId = sendTarget.idForLogging();
strictAssert( strictAssert(
window.textsecure.messaging, window.textsecure.messaging,
'sendContentMessageToGroup: textsecure.messaging not available!' 'sendContentMessageToGroup: textsecure.messaging not available!'
@ -152,19 +166,19 @@ export async function sendContentMessageToGroup({
isEnabled('desktop.sendSenderKey3') && isEnabled('desktop.sendSenderKey3') &&
ourConversation?.get('capabilities')?.senderKey && ourConversation?.get('capabilities')?.senderKey &&
RemoteConfig.isEnabled('desktop.senderKey.send') && RemoteConfig.isEnabled('desktop.senderKey.send') &&
isGroupV2(conversation.attributes) sendTarget.isValid()
) { ) {
try { try {
return await sendToGroupViaSenderKey({ return await sendToGroupViaSenderKey({
contentHint, contentHint,
contentMessage, contentMessage,
conversation,
isPartialSend, isPartialSend,
messageId, messageId,
online, online,
recipients, recipients,
recursionCount: 0, recursionCount: 0,
sendOptions, sendOptions,
sendTarget,
sendType, sendType,
timestamp, timestamp,
}); });
@ -194,9 +208,7 @@ export async function sendContentMessageToGroup({
sendType, sendType,
timestamp, timestamp,
}); });
const groupId = isGroupV2(conversation.attributes) const groupId = sendTarget.isGroupV2() ? sendTarget.getGroupId() : undefined;
? conversation.get('groupId')
: undefined;
return window.textsecure.messaging.sendGroupProto({ return window.textsecure.messaging.sendGroupProto({
contentHint, contentHint,
groupId, groupId,
@ -213,32 +225,32 @@ export async function sendContentMessageToGroup({
export async function sendToGroupViaSenderKey(options: { export async function sendToGroupViaSenderKey(options: {
contentHint: number; contentHint: number;
contentMessage: Proto.Content; contentMessage: Proto.Content;
conversation: ConversationModel;
isPartialSend?: boolean; isPartialSend?: boolean;
messageId: string | undefined; messageId: string | undefined;
online?: boolean; online?: boolean;
recipients: Array<string>; recipients: Array<string>;
recursionCount: number; recursionCount: number;
sendOptions?: SendOptionsType; sendOptions?: SendOptionsType;
sendTarget: SenderKeyTargetType;
sendType: SendTypesType; sendType: SendTypesType;
timestamp: number; timestamp: number;
}): Promise<CallbackResultType> { }): Promise<CallbackResultType> {
const { const {
contentHint, contentHint,
contentMessage, contentMessage,
conversation,
isPartialSend, isPartialSend,
messageId, messageId,
online, online,
recursionCount,
recipients, recipients,
recursionCount,
sendOptions, sendOptions,
sendTarget,
sendType, sendType,
timestamp, timestamp,
} = options; } = options;
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
const logId = conversation.idForLogging(); const logId = sendTarget.idForLogging();
log.info( log.info(
`sendToGroupViaSenderKey/${logId}: Starting ${timestamp}, recursion count ${recursionCount}...` `sendToGroupViaSenderKey/${logId}: Starting ${timestamp}, recursion count ${recursionCount}...`
); );
@ -249,10 +261,10 @@ export async function sendToGroupViaSenderKey(options: {
); );
} }
const groupId = conversation.get('groupId'); const groupId = sendTarget.getGroupId();
if (!groupId || !isGroupV2(conversation.attributes)) { if (!sendTarget.isValid()) {
throw new Error( throw new Error(
`sendToGroupViaSenderKey/${logId}: Missing groupId or group is not GV2` `sendToGroupViaSenderKey/${logId}: sendTarget is not valid!`
); );
} }
@ -271,29 +283,40 @@ export async function sendToGroupViaSenderKey(options: {
'sendToGroupViaSenderKey: textsecure.messaging not available!' 'sendToGroupViaSenderKey: textsecure.messaging not available!'
); );
const { attributes }: { attributes: ConversationAttributesType } =
conversation;
// 1. Add sender key info if we have none, or clear out if it's too old // 1. Add sender key info if we have none, or clear out if it's too old
const THIRTY_DAYS = 30 * DAY; const THIRTY_DAYS = 30 * DAY;
if (!attributes.senderKeyInfo) {
// Note: From here on, generally need to recurse if we change senderKeyInfo
const senderKeyInfo = sendTarget.getSenderKeyInfo();
if (!senderKeyInfo) {
log.info( log.info(
`sendToGroupViaSenderKey/${logId}: Adding initial sender key info` `sendToGroupViaSenderKey/${logId}: Adding initial sender key info`
); );
conversation.set({ await sendTarget.saveSenderKeyInfo({
senderKeyInfo: { createdAtDate: Date.now(),
createdAtDate: Date.now(), distributionId: UUID.generate().toString(),
distributionId: UUID.generate().toString(), memberDevices: [],
memberDevices: [],
},
}); });
window.Signal.Data.updateConversation(attributes);
} else if (isOlderThan(attributes.senderKeyInfo.createdAtDate, THIRTY_DAYS)) { // Restart here because we updated senderKeyInfo
const { createdAtDate } = attributes.senderKeyInfo; return sendToGroupViaSenderKey({
...options,
recursionCount: recursionCount + 1,
});
}
if (isOlderThan(senderKeyInfo.createdAtDate, THIRTY_DAYS)) {
const { createdAtDate } = senderKeyInfo;
log.info( log.info(
`sendToGroupViaSenderKey/${logId}: Resetting sender key; ${createdAtDate} is too old` `sendToGroupViaSenderKey/${logId}: Resetting sender key; ${createdAtDate} is too old`
); );
await resetSenderKey(conversation); await resetSenderKey(sendTarget);
// Restart here because we updated senderKeyInfo
return sendToGroupViaSenderKey({
...options,
recursionCount: recursionCount + 1,
});
} }
// 2. Fetch all devices we believe we'll be sending to // 2. Fetch all devices we believe we'll be sending to
@ -320,15 +343,8 @@ export async function sendToGroupViaSenderKey(options: {
}); });
} }
strictAssert( const { memberDevices, distributionId, createdAtDate } = senderKeyInfo;
attributes.senderKeyInfo, const memberSet = new Set(sendTarget.getMembers());
`sendToGroupViaSenderKey/${logId}: expect senderKeyInfo`
);
// Note: From here on, we will need to recurse if we change senderKeyInfo
const { memberDevices, distributionId, createdAtDate } =
attributes.senderKeyInfo;
const memberSet = new Set(conversation.getMembers());
// 4. Partition devices into sender key and non-sender key groups // 4. Partition devices into sender key and non-sender key groups
const [devicesForSenderKey, devicesForNormalSend] = partition( const [devicesForSenderKey, devicesForNormalSend] = partition(
@ -366,10 +382,10 @@ export async function sendToGroupViaSenderKey(options: {
// 7. If members have been removed from the group, we need to reset our sender key, then // 7. If members have been removed from the group, we need to reset our sender key, then
// start over to get a fresh set of target devices. // start over to get a fresh set of target devices.
const keyNeedsReset = Array.from(removedFromMemberUuids).some( const keyNeedsReset = Array.from(removedFromMemberUuids).some(
uuid => !conversation.hasMember(uuid) uuid => !sendTarget.hasMember(uuid)
); );
if (keyNeedsReset) { if (keyNeedsReset) {
await resetSenderKey(conversation); await resetSenderKey(sendTarget);
// Restart here to start over; empty memberDevices means we'll send distribution // Restart here to start over; empty memberDevices means we'll send distribution
// message to everyone. // message to everyone.
@ -403,14 +419,11 @@ export async function sendToGroupViaSenderKey(options: {
// Update memberDevices with new devices // Update memberDevices with new devices
const updatedMemberDevices = [...memberDevices, ...newToMemberDevices]; const updatedMemberDevices = [...memberDevices, ...newToMemberDevices];
conversation.set({ await sendTarget.saveSenderKeyInfo({
senderKeyInfo: { createdAtDate,
createdAtDate, distributionId,
distributionId, memberDevices: updatedMemberDevices,
memberDevices: updatedMemberDevices,
},
}); });
window.Signal.Data.updateConversation(conversation.attributes);
// Restart here because we might have discovered new or dropped devices as part of // Restart here because we might have discovered new or dropped devices as part of
// distributing our sender key. // distributing our sender key.
@ -430,14 +443,14 @@ export async function sendToGroupViaSenderKey(options: {
), ),
]; ];
conversation.set({ await sendTarget.saveSenderKeyInfo({
senderKeyInfo: { createdAtDate,
createdAtDate, distributionId,
distributionId, memberDevices: updatedMemberDevices,
memberDevices: updatedMemberDevices,
},
}); });
window.Signal.Data.updateConversation(conversation.attributes);
// Note, we do not need to restart here because we don't refer back to senderKeyInfo
// after this point.
} }
// 10. Send the Sender Key message! // 10. Send the Sender Key message!
@ -513,7 +526,7 @@ export async function sendToGroupViaSenderKey(options: {
}); });
} }
if (error.code === ERROR_STALE_DEVICES) { if (error.code === ERROR_STALE_DEVICES) {
await handle410Response(conversation, error); await handle410Response(sendTarget, error);
// Restart here to use the right registrationIds for devices we already knew about, // Restart here to use the right registrationIds for devices we already knew about,
// as well as send our sender key to these re-registered or re-linked devices. // as well as send our sender key to these re-registered or re-linked devices.
@ -591,7 +604,7 @@ export async function sendToGroupViaSenderKey(options: {
const recipientUuid = sentToConversation.get('uuid'); const recipientUuid = sentToConversation.get('uuid');
if (!recipientUuid) { if (!recipientUuid) {
log.warn( log.warn(
`sendToGroupViaSenderKey/callback: Conversation ${conversation.idForLogging()} had no UUID` `sendToGroupViaSenderKey/callback: Conversation ${sentToConversation.idForLogging()} had no UUID`
); );
return; return;
} }
@ -730,10 +743,10 @@ async function handle409Response(logId: string, error: HTTPError) {
} }
async function handle410Response( async function handle410Response(
conversation: ConversationModel, sendTarget: SenderKeyTargetType,
error: HTTPError error: HTTPError
) { ) {
const logId = conversation.idForLogging(); const logId = sendTarget.idForLogging();
const parsed = multiRecipient410ResponseSchema.safeParse(error.response); const parsed = multiRecipient410ResponseSchema.safeParse(error.response);
if (parsed.success) { if (parsed.success) {
@ -757,21 +770,18 @@ async function handle410Response(
// Forget that we've sent our sender key to these devices, since they've // Forget that we've sent our sender key to these devices, since they've
// been re-registered or re-linked. // been re-registered or re-linked.
const senderKeyInfo = conversation.get('senderKeyInfo'); const senderKeyInfo = sendTarget.getSenderKeyInfo();
if (senderKeyInfo) { if (senderKeyInfo) {
const devicesToRemove: Array<PartialDeviceType> = const devicesToRemove: Array<PartialDeviceType> =
devices.staleDevices.map(id => ({ id, identifier: uuid })); devices.staleDevices.map(id => ({ id, identifier: uuid }));
conversation.set({ await sendTarget.saveSenderKeyInfo({
senderKeyInfo: { ...senderKeyInfo,
...senderKeyInfo, memberDevices: differenceWith(
memberDevices: differenceWith( senderKeyInfo.memberDevices,
senderKeyInfo.memberDevices, devicesToRemove,
devicesToRemove, partialDeviceComparator
partialDeviceComparator ),
),
},
}); });
window.Signal.Data.updateConversation(conversation.attributes);
} }
} }
}), }),
@ -836,7 +846,7 @@ async function encryptForSenderKey({
contentMessage: Uint8Array; contentMessage: Uint8Array;
devices: Array<DeviceType>; devices: Array<DeviceType>;
distributionId: string; distributionId: string;
groupId: string; groupId?: string;
}): Promise<Buffer> { }): Promise<Buffer> {
const ourUuid = window.textsecure.storage.user.getCheckedUuid(); const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const ourDeviceId = window.textsecure.storage.user.getDeviceId(); const ourDeviceId = window.textsecure.storage.user.getDeviceId();
@ -860,7 +870,7 @@ async function encryptForSenderKey({
() => groupEncrypt(sender, distributionId, senderKeyStore, message) () => groupEncrypt(sender, distributionId, senderKeyStore, message)
); );
const groupIdBuffer = Buffer.from(groupId, 'base64'); const groupIdBuffer = groupId ? Buffer.from(groupId, 'base64') : null;
const senderCertificateObject = await senderCertificateService.get( const senderCertificateObject = await senderCertificateService.get(
SenderCertificateMode.WithoutE164 SenderCertificateMode.WithoutE164
); );
@ -1027,13 +1037,11 @@ function getOurAddress(): Address {
return new Address(ourUuid, ourDeviceId); return new Address(ourUuid, ourDeviceId);
} }
async function resetSenderKey(conversation: ConversationModel): Promise<void> { async function resetSenderKey(sendTarget: SenderKeyTargetType): Promise<void> {
const logId = conversation.idForLogging(); const logId = sendTarget.idForLogging();
log.info(`resetSenderKey/${logId}: Sender key needs reset. Clearing data...`); log.info(`resetSenderKey/${logId}: Sender key needs reset. Clearing data...`);
const { attributes }: { attributes: ConversationAttributesType } = const senderKeyInfo = sendTarget.getSenderKeyInfo();
conversation;
const { senderKeyInfo } = attributes;
if (!senderKeyInfo) { if (!senderKeyInfo) {
log.warn(`resetSenderKey/${logId}: No sender key info`); log.warn(`resetSenderKey/${logId}: No sender key info`);
return; return;
@ -1043,14 +1051,11 @@ async function resetSenderKey(conversation: ConversationModel): Promise<void> {
const ourAddress = getOurAddress(); const ourAddress = getOurAddress();
// Note: We preserve existing distributionId to minimize space for sender key storage // Note: We preserve existing distributionId to minimize space for sender key storage
conversation.set({ await sendTarget.saveSenderKeyInfo({
senderKeyInfo: { createdAtDate: Date.now(),
createdAtDate: Date.now(), distributionId,
distributionId, memberDevices: [],
memberDevices: [],
},
}); });
window.Signal.Data.updateConversation(conversation.attributes);
const ourUuid = window.storage.user.getCheckedUuid(); const ourUuid = window.storage.user.getCheckedUuid();
await window.textsecure.storage.protocol.removeSenderKey( await window.textsecure.storage.protocol.removeSenderKey(