Fix group send endorsements for new members

This commit is contained in:
Jamie Kyle 2024-09-16 12:37:38 -07:00 committed by GitHub
parent a40d54099c
commit 65539b1419
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 106 additions and 43 deletions

View file

@ -3986,7 +3986,7 @@ async function updateGroupViaLogs({
if (
cachedEndorsementsExpiration != null &&
!isValidGroupSendEndorsementsExpiration(cachedEndorsementsExpiration)
!isValidGroupSendEndorsementsExpiration(cachedEndorsementsExpiration * 1000)
) {
log.info(
`updateGroupViaLogs/${logId}: Group had invalid endorsements expiration (${cachedEndorsementsExpiration}), fetching new endorsements`

View file

@ -147,8 +147,24 @@ export class App extends EventEmitter {
);
}
private async checkForFatalTestErrors(): Promise<void> {
const count = await this.getPendingEventCount('fatalTestError');
if (count === 0) {
return;
}
for (let i = 0; i < count; i += 1) {
// eslint-disable-next-line no-await-in-loop, no-console
console.error(await this.waitForEvent('fatalTestError'));
}
throw new Error('App had fatal test errors');
}
public async close(): Promise<void> {
await this.app.close();
try {
await this.checkForFatalTestErrors();
} finally {
await this.app.close();
}
}
public async getWindow(): Promise<Page> {

View file

@ -25,6 +25,8 @@ import type { GroupV2MemberType } from '../model-types';
import { DurationInSeconds } from './durations';
import { ToastType } from '../types/Toast';
import * as Errors from '../types/errors';
import { isTestOrMockEnvironment } from '../environment';
import { isAlpha } from './version';
export function decodeGroupSendEndorsementResponse({
groupId,
@ -116,6 +118,14 @@ export function decodeGroupSendEndorsementResponse({
const TWO_DAYS = DurationInSeconds.fromDays(2);
const TWO_HOURS = DurationInSeconds.fromHours(2);
function logServiceIds(list: Iterable<string>) {
const items = Array.from(list);
if (items.length <= 5) {
return items.join(', ');
}
return `${items.slice(0, 4).join(', ')}, and ${items.length - 4} others`;
}
export function isValidGroupSendEndorsementsExpiration(
expiration: number
): boolean {
@ -127,11 +137,11 @@ export function isValidGroupSendEndorsementsExpiration(
export class GroupSendEndorsementState {
#combinedEndorsement: GroupSendCombinedEndorsementRecord;
#memberEndorsements = new Map<
#otherMemberEndorsements = new Map<
ServiceIdString,
GroupSendMemberEndorsementRecord
>();
#memberEndorsementsAcis = new Set<AciString>();
#otherMemberEndorsementsAcis = new Set<AciString>();
#groupSecretParamsBase64: string;
#endorsementCache = new WeakMap<Uint8Array, GroupSendEndorsement>();
@ -140,11 +150,15 @@ export class GroupSendEndorsementState {
groupSecretParamsBase64: string
) {
this.#combinedEndorsement = data.combinedEndorsement;
for (const endorsement of data.memberEndorsements) {
this.#memberEndorsements.set(endorsement.memberAci, endorsement);
this.#memberEndorsementsAcis.add(endorsement.memberAci);
}
this.#groupSecretParamsBase64 = groupSecretParamsBase64;
const ourAci = window.textsecure.storage.user.getCheckedAci();
for (const endorsement of data.memberEndorsements) {
if (endorsement.memberAci !== ourAci) {
this.#otherMemberEndorsements.set(endorsement.memberAci, endorsement);
this.#otherMemberEndorsementsAcis.add(endorsement.memberAci);
}
}
}
isSafeExpirationRange(): boolean {
@ -158,7 +172,7 @@ export class GroupSendEndorsementState {
}
hasMember(serviceId: ServiceIdString): boolean {
return this.#memberEndorsements.has(serviceId);
return this.#otherMemberEndorsements.has(serviceId);
}
#toEndorsement(contents: Uint8Array) {
@ -173,19 +187,12 @@ export class GroupSendEndorsementState {
// Strategy 1: Faster when we're sending to most of the group members
// `combined.byRemoving(combine(difference(members, sends)))`
#subtractMemberEndorsements(
serviceIds: Set<ServiceIdString>
difference: Set<ServiceIdString>
): GroupSendEndorsement {
const difference = this.#memberEndorsementsAcis.difference(serviceIds);
const ourAci = window.textsecure.storage.user.getCheckedAci();
const toRemove: Array<GroupSendEndorsement> = [];
for (const serviceId of difference) {
if (serviceId === ourAci) {
// Note: Combined endorsement does not include our aci
continue;
}
const memberEndorsement = this.#memberEndorsements.get(serviceId);
for (const serviceId of difference) {
const memberEndorsement = this.#otherMemberEndorsements.get(serviceId);
strictAssert(
memberEndorsement,
'serializeGroupSendEndorsementFullToken: Missing endorsement'
@ -204,7 +211,7 @@ export class GroupSendEndorsementState {
serviceIds: Set<ServiceIdString>
): GroupSendEndorsement {
const memberEndorsements = Array.from(serviceIds).map(serviceId => {
const memberEndorsement = this.#memberEndorsements.get(serviceId);
const memberEndorsement = this.#otherMemberEndorsements.get(serviceId);
strictAssert(
memberEndorsement,
'serializeGroupSendEndorsementFullToken: Missing endorsement'
@ -217,17 +224,28 @@ export class GroupSendEndorsementState {
buildToken(serviceIds: Set<ServiceIdString>): GroupSendToken {
const sendCount = serviceIds.size;
const memberCount = this.#memberEndorsements.size;
const logId = `GroupSendEndorsementState.buildToken(${sendCount} of ${memberCount})`;
const otherMemberCount = this.#otherMemberEndorsements.size;
const logId = `GroupSendEndorsementState.buildToken(${sendCount} of ${otherMemberCount})`;
const missing = serviceIds.difference(this.#otherMemberEndorsementsAcis);
if (missing.size !== 0) {
throw new Error(
`Attempted to build token with memberAcis we don't have endorsements for (${logServiceIds(missing)})`
);
}
const difference = this.#otherMemberEndorsementsAcis.difference(serviceIds);
log.info(
`buildToken: Endorsements without sends ${difference.size}: ${logServiceIds(difference)})`
);
let endorsement: GroupSendEndorsement;
if (sendCount === memberCount - 1) {
if (difference.size === 0) {
log.info(`${logId}: combinedEndorsement`);
// Note: Combined endorsement does not include our aci
endorsement = this.#toEndorsement(this.#combinedEndorsement.endorsement);
} else if (sendCount > (memberCount - 1) / 2) {
} else if (difference.size < otherMemberCount / 2) {
log.info(`${logId}: subtractMemberEndorsements`);
endorsement = this.#subtractMemberEndorsements(serviceIds);
endorsement = this.#subtractMemberEndorsements(difference);
} else {
log.info(`${logId}: combineMemberEndorsements`);
endorsement = this.#combineMemberEndorsements(serviceIds);
@ -251,8 +269,13 @@ export class GroupSendEndorsementState {
export function onFailedToSendWithEndorsements(error: Error): void {
log.error('onFailedToSendWithEndorsements', Errors.toLogFormat(error));
window.reduxActions.toast.showToast({
toastType: ToastType.FailedToSendWithEndorsements,
});
if (isTestOrMockEnvironment() || isAlpha(window.getVersion())) {
window.reduxActions.toast.showToast({
toastType: ToastType.FailedToSendWithEndorsements,
});
}
if (window.SignalCI) {
window.SignalCI.handleEvent('fatalTestError', error);
}
assertDev(false, 'We should never fail to send with endorsements');
}

View file

@ -72,6 +72,7 @@ import {
} from './groupSendEndorsements';
import { maybeUpdateGroup } from '../groups';
import type { GroupSendToken } from '../types/GroupSendEndorsements';
import { isAciString } from './isAciString';
const UNKNOWN_RECIPIENT = 404;
const INCORRECT_AUTH_KEY = 401;
@ -361,16 +362,18 @@ export async function sendToGroupViaSenderKey(
let groupSendEndorsementState: GroupSendEndorsementState | null = null;
if (groupId != null) {
strictAssert(conversation, 'Must have conversation for endorsements');
const data = await DataReader.getGroupSendEndorsementsData(groupId);
if (data == null) {
onFailedToSendWithEndorsements(
new Error(
`sendToGroupViaSenderKey/${logId}: Missing all endorsements for group`
)
);
if (conversation.isMember(ourAci)) {
onFailedToSendWithEndorsements(
new Error(
`sendToGroupViaSenderKey/${logId}: Missing all endorsements for group`
)
);
}
} else {
strictAssert(conversation, 'Must have conversation for endorsements');
log.info(
`sendToGroupViaSenderKey/${logId}: Loaded endorsements for ${data.memberEndorsements.length} members`
);
@ -572,9 +575,13 @@ export async function sendToGroupViaSenderKey(
let accessKeys: Buffer | undefined;
if (groupSendEndorsementState != null) {
strictAssert(conversation, 'Must have conversation for endorsements');
groupSendToken = groupSendEndorsementState.buildToken(
new Set(senderKeyRecipients)
);
try {
groupSendToken = groupSendEndorsementState.buildToken(
new Set(senderKeyRecipients)
);
} catch (error) {
onFailedToSendWithEndorsements(error);
}
} else {
accessKeys = getXorOfAccessKeys(devicesForSenderKey, { story });
}
@ -689,6 +696,10 @@ export async function sendToGroupViaSenderKey(
}
}
if (groupSendEndorsementState != null) {
onFailedToSendWithEndorsements(error);
}
log.error(
`sendToGroupViaSenderKey/${logId}: Returned unexpected error code: ${
error.code
@ -1259,8 +1270,7 @@ function isValidSenderKeyRecipient(
}
if (groupSendEndorsementState != null) {
const memberEndorsement = groupSendEndorsementState.hasMember(serviceId);
if (memberEndorsement == null) {
if (!groupSendEndorsementState.hasMember(serviceId)) {
onFailedToSendWithEndorsements(
new Error(
`isValidSenderKeyRecipient: Sending to ${serviceId}, missing endorsement`
@ -1429,6 +1439,12 @@ async function fetchKeysForServiceId(
'private'
);
let useGroupSendEndorsement = isAciString(serviceId);
if (!groupSendEndorsementState?.hasMember(serviceId)) {
log.error(`fetchKeysForServiceId: ${serviceId} does not have endorsements`);
useGroupSendEndorsement = false;
}
try {
// Note: we have no way to make an unrestricted unauthenticated key fetch as part of a
// story send, so we hardcode story=false.
@ -1436,8 +1452,13 @@ async function fetchKeysForServiceId(
story: false,
});
const groupSendToken =
groupSendEndorsementState?.buildToken(new Set([serviceId])) ?? null;
let groupSendToken: GroupSendToken | null = null;
if (useGroupSendEndorsement && groupSendEndorsementState != null) {
groupSendToken = groupSendEndorsementState.buildToken(
new Set([serviceId])
);
}
const { accessKeyFailed } = await getKeysForServiceId(
serviceId,
@ -1456,6 +1477,9 @@ async function fetchKeysForServiceId(
await DataWriter.updateConversation(emptyConversation.attributes);
}
} catch (error: unknown) {
if (useGroupSendEndorsement) {
onFailedToSendWithEndorsements(error as Error);
}
if (error instanceof UnregisteredUserError) {
await markServiceIdUnregistered(serviceId);
return;