Support endorsements for group 1:1 sends

Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
Jamie Kyle 2024-10-10 10:57:22 -07:00 committed by GitHub
parent 76a77a9b7f
commit e617981e59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1296 additions and 796 deletions

View file

@ -23,7 +23,7 @@ import {
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import * as Errors from '../types/errors';
import { DataWriter, DataReader } from '../sql/Client';
import { DataWriter } from '../sql/Client';
import { getValue } from '../RemoteConfig';
import type { ServiceIdString } from '../types/ServiceId';
import { ServiceIdKind } from '../types/ServiceId';
@ -66,11 +66,11 @@ import { strictAssert } from './assert';
import * as log from '../logging/log';
import { GLOBAL_ZONE } from '../SignalProtocolStore';
import { waitForAll } from './waitForAll';
import type { GroupSendEndorsementState } from './groupSendEndorsements';
import {
GroupSendEndorsementState,
maybeCreateGroupSendEndorsementState,
onFailedToSendWithEndorsements,
} from './groupSendEndorsements';
import { maybeUpdateGroup } from '../groups';
import type { GroupSendToken } from '../types/GroupSendEndorsements';
import { isAciString } from './isAciString';
import { safeParseStrict, safeParseUnknown } from './schemas';
@ -213,11 +213,11 @@ export async function sendContentMessageToGroup(
if (sendTarget.isValid()) {
try {
return await sendToGroupViaSenderKey(
options,
0,
'init (sendContentMessageToGroup)'
);
return await sendToGroupViaSenderKey(options, {
count: 0,
didRefreshGroupState: false,
reason: 'init (sendContentMessageToGroup)',
});
} catch (error: unknown) {
if (!(error instanceof Error)) {
throw error;
@ -259,11 +259,27 @@ export async function sendContentMessageToGroup(
// The Primary Sender Key workflow
type SendRecursion = {
count: number;
didRefreshGroupState: boolean;
reason: string;
};
export async function sendToGroupViaSenderKey(
options: SendToGroupOptions,
recursionCount: number,
recursionReason: string
recursion: SendRecursion
): Promise<CallbackResultType> {
function startOver(
reason: string,
didRefreshGroupState = recursion.didRefreshGroupState
) {
return sendToGroupViaSenderKey(options, {
count: recursion.count + 1,
didRefreshGroupState,
reason,
});
}
const {
contentHint,
contentMessage,
@ -282,12 +298,12 @@ export async function sendToGroupViaSenderKey(
const logId = sendTarget.idForLogging();
log.info(
`sendToGroupViaSenderKey/${logId}: Starting ${timestamp}, recursion count ${recursionCount}, reason: ${recursionReason}...`
`sendToGroupViaSenderKey/${logId}: Starting ${timestamp}, recursion count ${recursion.count}, reason: ${recursion.reason}...`
);
if (recursionCount > MAX_RECURSION) {
if (recursion.count > MAX_RECURSION) {
throw new Error(
`sendToGroupViaSenderKey/${logId}: Too much recursion! Count is at ${recursionCount}`
`sendToGroupViaSenderKey/${logId}: Too much recursion! Count is at ${recursion.count}`
);
}
@ -328,11 +344,7 @@ export async function sendToGroupViaSenderKey(
});
// Restart here because we updated senderKeyInfo
return sendToGroupViaSenderKey(
options,
recursionCount + 1,
'Added missing sender key info'
);
return startOver('Added missing sender key info');
}
const EXPIRE_DURATION = getSenderKeyExpireDuration();
@ -344,11 +356,7 @@ export async function sendToGroupViaSenderKey(
await resetSenderKey(sendTarget);
// Restart here because we updated senderKeyInfo
return sendToGroupViaSenderKey(
options,
recursionCount + 1,
'sender key info expired'
);
return startOver('sender key info expired');
}
// 2. Fetch all devices we believe we'll be sending to
@ -356,49 +364,20 @@ export async function sendToGroupViaSenderKey(
const { devices: currentDevices, emptyServiceIds } =
await window.textsecure.storage.protocol.getOpenDevices(ourAci, recipients);
const conversation =
groupId != null
? (window.ConversationController.get(groupId) ?? null)
: null;
let groupSendEndorsementState: GroupSendEndorsementState | null = null;
if (groupId != null) {
strictAssert(conversation, 'Must have conversation for endorsements');
const data = await DataReader.getGroupSendEndorsementsData(groupId);
if (data == null) {
if (conversation.isMember(ourAci)) {
onFailedToSendWithEndorsements(
new Error(
`sendToGroupViaSenderKey/${logId}: Missing all endorsements for group`
)
);
}
} else {
log.info(
`sendToGroupViaSenderKey/${logId}: Loaded endorsements for ${data.memberEndorsements.length} members`
if (groupId != null && !story) {
const { state, didRefreshGroupState } =
await maybeCreateGroupSendEndorsementState(
groupId,
recursion.didRefreshGroupState
);
const groupSecretParamsBase64 = conversation.get('secretParams');
strictAssert(groupSecretParamsBase64, 'Must have secret params');
groupSendEndorsementState = new GroupSendEndorsementState(
data,
groupSecretParamsBase64
if (state != null) {
groupSendEndorsementState = state;
} else if (didRefreshGroupState) {
return startOver(
'group send endorsements outside expiration range',
true
);
if (
groupSendEndorsementState != null &&
!groupSendEndorsementState.isSafeExpirationRange()
) {
log.info(
`sendToGroupViaSenderKey/${logId}: Endorsements close to expiration (${groupSendEndorsementState.getExpiration().getTime()}, ${Date.now()}), refreshing group`
);
await maybeUpdateGroup({ conversation });
return sendToGroupViaSenderKey(
options,
recursionCount + 1,
'group send endorsements outside expiration range'
);
}
}
}
@ -412,11 +391,7 @@ export async function sendToGroupViaSenderKey(
await fetchKeysForServiceIds(emptyServiceIds, groupSendEndorsementState);
// Restart here to capture devices for accounts we just started sessions with
return sendToGroupViaSenderKey(
options,
recursionCount + 1,
'fetched prekey bundles'
);
return startOver('fetched prekey bundles');
}
const { memberDevices, distributionId, createdAtDate } = senderKeyInfo;
@ -478,11 +453,7 @@ export async function sendToGroupViaSenderKey(
// Restart here to start over; empty memberDevices means we'll send distribution
// message to everyone.
return sendToGroupViaSenderKey(
options,
recursionCount + 1,
'removed members in send target'
);
return startOver('removed members in send target');
}
// 8. If there are new members or new devices in the group, we need to ensure that they
@ -533,11 +504,7 @@ export async function sendToGroupViaSenderKey(
// Restart here because we might have discovered new or dropped devices as part of
// distributing our sender key.
return sendToGroupViaSenderKey(
options,
recursionCount + 1,
'sent skdm to new members'
);
return startOver('sent skdm to new members');
}
// 9. Update memberDevices with removals which didn't require a reset.
@ -572,17 +539,12 @@ export async function sendToGroupViaSenderKey(
senderKeyRecipientsWithDevices[serviceId].push(id);
});
let groupSendToken: GroupSendToken | undefined;
let accessKeys: Buffer | undefined;
let groupSendToken: GroupSendToken | null = null;
let accessKeys: Buffer | null = null;
if (groupSendEndorsementState != null) {
strictAssert(conversation, 'Must have conversation for endorsements');
try {
groupSendToken = groupSendEndorsementState.buildToken(
new Set(senderKeyRecipients)
);
} catch (error) {
onFailedToSendWithEndorsements(error);
}
groupSendToken = groupSendEndorsementState.buildToken(
new Set(senderKeyRecipients)
);
} else {
accessKeys = getXorOfAccessKeys(devicesForSenderKey, { story });
}
@ -656,22 +618,14 @@ export async function sendToGroupViaSenderKey(
await handle409Response(sendTarget, groupSendEndorsementState, error);
// Restart here to capture the right set of devices for our next send.
return sendToGroupViaSenderKey(
options,
recursionCount + 1,
'error: expired or missing devices'
);
return startOver('error: expired or missing devices');
}
if (error.code === ERROR_STALE_DEVICES) {
await handle410Response(sendTarget, groupSendEndorsementState, error);
// 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.
return sendToGroupViaSenderKey(
options,
recursionCount + 1,
'error: stale devices'
);
return startOver('error: stale devices');
}
if (
error instanceof LibSignalErrorBase &&
@ -689,11 +643,7 @@ export async function sendToGroupViaSenderKey(
await DataWriter.updateConversation(brokenAccount.attributes);
// Now that we've eliminate this problematic account, we can try the send again.
return sendToGroupViaSenderKey(
options,
recursionCount + 1,
'error: invalid registration id'
);
return startOver('error: invalid registration id');
}
}