Refresh group state if missing group members

This commit is contained in:
Jamie Kyle 2024-10-28 09:23:37 -07:00 committed by GitHub
parent 872f1bd843
commit 40ac288a3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 73 additions and 48 deletions

View file

@ -98,7 +98,7 @@ import { sleep } from './util/sleep';
import { groupInvitesRoute } from './util/signalRoutes';
import {
decodeGroupSendEndorsementResponse,
isValidGroupSendEndorsementsExpiration,
validateGroupSendEndorsementsExpiration,
} from './util/groupSendEndorsements';
import { getProfile } from './util/getProfile';
import { generateMessageId } from './util/generateMessageId';
@ -3992,7 +3992,7 @@ async function updateGroupViaLogs({
await DataReader.getGroupSendCombinedEndorsementExpiration(groupId);
if (cachedEndorsementsExpiration != null) {
const result = isValidGroupSendEndorsementsExpiration(
const result = validateGroupSendEndorsementsExpiration(
cachedEndorsementsExpiration * 1000
);
if (!result.valid) {

View file

@ -2,14 +2,14 @@
// SPDX-License-Identifier: AGPL-3.0-only
import assert from 'node:assert/strict';
import { isValidGroupSendEndorsementsExpiration } from '../../util/groupSendEndorsements';
import { validateGroupSendEndorsementsExpiration } from '../../util/groupSendEndorsements';
import { DAY, HOUR, SECOND } from '../../util/durations';
describe('groupSendEndorsements', () => {
describe('isValidGroupSendEndorsementsExpiration', () => {
describe('validateGroupSendEndorsementsExpiration', () => {
function validateDistance(distance: number) {
const expiration = Date.now() + distance;
return isValidGroupSendEndorsementsExpiration(expiration);
return validateGroupSendEndorsementsExpiration(expiration);
}
function checkValid(label: string, distance: number) {

View file

@ -129,13 +129,13 @@ function logServiceIds(list: Iterable<string>) {
return `${items.slice(0, 4).join(', ')}, and ${items.length - 4} others`;
}
export type EndorsementsExpirationValidationResult =
export type ValidationResult =
| { valid: true; reason?: never }
| { valid: false; reason: string };
export function isValidGroupSendEndorsementsExpiration(
export function validateGroupSendEndorsementsExpiration(
expiration: number
): EndorsementsExpirationValidationResult {
): ValidationResult {
const expSeconds = DurationInSeconds.fromMillis(expiration);
const nowSeconds = DurationInSeconds.fromMillis(Date.now());
const info = `now: ${nowSeconds}, exp: ${expSeconds}`;
@ -202,10 +202,13 @@ export class GroupSendEndorsementState {
);
const expiration = this.getExpiration();
const result = validateGroupSendEndorsementsExpiration(
expiration.getTime()
);
strictAssert(
isValidGroupSendEndorsementsExpiration(expiration.getTime()),
`${this.#logId}: toToken: Cannot build token with invalid expiration`
result.valid,
`${this.#logId}: toToken: Endorsements are expired (${result.reason})`
);
const fullToken = endorsement.toFullToken(groupSecretParams, expiration);
@ -341,69 +344,91 @@ type MaybeCreateGroupSendEndorsementStateResult =
| { state: GroupSendEndorsementState; didRefreshGroupState: false }
| { state: null; didRefreshGroupState: boolean };
function validateGroupSendEndorsements(
members: Set<ServiceIdString>,
state: GroupSendEndorsementState | null
): ValidationResult {
// Check if we should have endorsements (pending members should not)
if (state == null) {
const ourAci = window.textsecure.storage.user.getCheckedAci();
if (members.has(ourAci)) {
return { valid: false, reason: 'missing all endorsements' };
}
return { valid: true };
}
// Check for expired endorsements
const expiration = state.getExpiration().getTime();
const expirationResult = validateGroupSendEndorsementsExpiration(expiration);
if (!expirationResult.valid) {
return { valid: false, reason: expirationResult.reason };
}
// Check for missing endorsements
const missing = new Set<ServiceIdString>();
for (const member of members) {
if (!state.hasMember(member)) {
missing.add(member);
}
}
if (missing.size > 0) {
return {
valid: false,
reason: `missing ${missing.size} endorsements (${logServiceIds(missing)})`,
};
}
return { valid: true };
}
export async function maybeCreateGroupSendEndorsementState(
groupId: string,
alreadyRefreshedGroupState: boolean
): Promise<MaybeCreateGroupSendEndorsementStateResult> {
const logId = `maybeCreateGroupSendEndorsementState/groupv2(${groupId})`;
const conversation = window.ConversationController.get(groupId);
strictAssert(
conversation != null,
'maybeCreateGroupSendEndorsementState: Convertion not found'
);
const logId = `maybeCreateGroupSendEndorsementState/${conversation.idForLogging()}`;
strictAssert(conversation != null, `${logId}: Convertion not found`);
strictAssert(
isGroupV2(conversation.attributes),
`${logId}: Conversation is not groupV2`
);
const secretParams = conversation.get('secretParams');
const membersV2 = conversation.get('membersV2');
strictAssert(secretParams, `${logId}: Must have secret params`);
strictAssert(membersV2, `${logId}: Must have members`);
const members = new Set(membersV2.map(member => member.aci));
const data = await DataReader.getGroupSendEndorsementsData(groupId);
if (data == null) {
const ourAci = window.textsecure.storage.user.getCheckedAci();
if (conversation.isMember(ourAci)) {
if (!alreadyRefreshedGroupState) {
log.info(`${logId}: Missing endorsements for group, refreshing group`);
await window.waitForEmptyEventQueue();
await maybeUpdateGroup({ conversation, force: true });
return { state: null, didRefreshGroupState: true };
}
onFailedToSendWithEndorsements(
new Error(`${logId}: Missing all endorsements for group`)
);
}
return { state: null, didRefreshGroupState: false };
}
const groupSecretParamsBase64 = conversation.get('secretParams');
strictAssert(groupSecretParamsBase64, `${logId}: Must have secret params`);
const groupSendEndorsementState = new GroupSendEndorsementState(
data,
groupSecretParamsBase64
);
const result = isValidGroupSendEndorsementsExpiration(
groupSendEndorsementState.getExpiration().getTime()
);
const state =
data != null ? new GroupSendEndorsementState(data, secretParams) : null;
// Check if we need to refresh the group state.
const result = validateGroupSendEndorsements(members, state);
if (!result.valid) {
// If we've already refreshed the group state, we should log and move on.
if (alreadyRefreshedGroupState) {
onFailedToSendWithEndorsements(
new Error(
`${logId}: Endorsements are expired after refreshing group (${result.reason})`
`${logId}: Endorsements invalid after refreshing group: ${result.reason}`
)
);
return { state: null, didRefreshGroupState: false };
}
log.info(
`${logId}: Endorsements are expired (${result.reason}), refreshing group`
`${logId}: Endorsements invalid, refreshing group: ${result.reason}`
);
// Wait for all incoming messages to be processed before refreshing
// the group state to avoid incorrectly processing messages.
await window.waitForEmptyEventQueue();
// Refresh the group state all allow the caller to try again.
await maybeUpdateGroup({ conversation, force: true });
return { state: null, didRefreshGroupState: true };
}
return { state: groupSendEndorsementState, didRefreshGroupState: false };
return { state, didRefreshGroupState: false };
}