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

View file

@ -2,14 +2,14 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { isValidGroupSendEndorsementsExpiration } from '../../util/groupSendEndorsements'; import { validateGroupSendEndorsementsExpiration } from '../../util/groupSendEndorsements';
import { DAY, HOUR, SECOND } from '../../util/durations'; import { DAY, HOUR, SECOND } from '../../util/durations';
describe('groupSendEndorsements', () => { describe('groupSendEndorsements', () => {
describe('isValidGroupSendEndorsementsExpiration', () => { describe('validateGroupSendEndorsementsExpiration', () => {
function validateDistance(distance: number) { function validateDistance(distance: number) {
const expiration = Date.now() + distance; const expiration = Date.now() + distance;
return isValidGroupSendEndorsementsExpiration(expiration); return validateGroupSendEndorsementsExpiration(expiration);
} }
function checkValid(label: string, distance: number) { 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`; return `${items.slice(0, 4).join(', ')}, and ${items.length - 4} others`;
} }
export type EndorsementsExpirationValidationResult = export type ValidationResult =
| { valid: true; reason?: never } | { valid: true; reason?: never }
| { valid: false; reason: string }; | { valid: false; reason: string };
export function isValidGroupSendEndorsementsExpiration( export function validateGroupSendEndorsementsExpiration(
expiration: number expiration: number
): EndorsementsExpirationValidationResult { ): ValidationResult {
const expSeconds = DurationInSeconds.fromMillis(expiration); const expSeconds = DurationInSeconds.fromMillis(expiration);
const nowSeconds = DurationInSeconds.fromMillis(Date.now()); const nowSeconds = DurationInSeconds.fromMillis(Date.now());
const info = `now: ${nowSeconds}, exp: ${expSeconds}`; const info = `now: ${nowSeconds}, exp: ${expSeconds}`;
@ -202,10 +202,13 @@ export class GroupSendEndorsementState {
); );
const expiration = this.getExpiration(); const expiration = this.getExpiration();
const result = validateGroupSendEndorsementsExpiration(
expiration.getTime()
);
strictAssert( strictAssert(
isValidGroupSendEndorsementsExpiration(expiration.getTime()), result.valid,
`${this.#logId}: toToken: Cannot build token with invalid expiration` `${this.#logId}: toToken: Endorsements are expired (${result.reason})`
); );
const fullToken = endorsement.toFullToken(groupSecretParams, expiration); const fullToken = endorsement.toFullToken(groupSecretParams, expiration);
@ -341,69 +344,91 @@ type MaybeCreateGroupSendEndorsementStateResult =
| { state: GroupSendEndorsementState; didRefreshGroupState: false } | { state: GroupSendEndorsementState; didRefreshGroupState: false }
| { state: null; didRefreshGroupState: boolean }; | { 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( export async function maybeCreateGroupSendEndorsementState(
groupId: string, groupId: string,
alreadyRefreshedGroupState: boolean alreadyRefreshedGroupState: boolean
): Promise<MaybeCreateGroupSendEndorsementStateResult> { ): Promise<MaybeCreateGroupSendEndorsementStateResult> {
const logId = `maybeCreateGroupSendEndorsementState/groupv2(${groupId})`;
const conversation = window.ConversationController.get(groupId); const conversation = window.ConversationController.get(groupId);
strictAssert( strictAssert(conversation != null, `${logId}: Convertion not found`);
conversation != null,
'maybeCreateGroupSendEndorsementState: Convertion not found'
);
const logId = `maybeCreateGroupSendEndorsementState/${conversation.idForLogging()}`;
strictAssert( strictAssert(
isGroupV2(conversation.attributes), isGroupV2(conversation.attributes),
`${logId}: Conversation is not groupV2` `${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); const data = await DataReader.getGroupSendEndorsementsData(groupId);
if (data == null) { const state =
const ourAci = window.textsecure.storage.user.getCheckedAci(); data != null ? new GroupSendEndorsementState(data, secretParams) : null;
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()
);
// Check if we need to refresh the group state.
const result = validateGroupSendEndorsements(members, state);
if (!result.valid) { if (!result.valid) {
// If we've already refreshed the group state, we should log and move on.
if (alreadyRefreshedGroupState) { if (alreadyRefreshedGroupState) {
onFailedToSendWithEndorsements( onFailedToSendWithEndorsements(
new Error( new Error(
`${logId}: Endorsements are expired after refreshing group (${result.reason})` `${logId}: Endorsements invalid after refreshing group: ${result.reason}`
) )
); );
return { state: null, didRefreshGroupState: false }; return { state: null, didRefreshGroupState: false };
} }
log.info( 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(); await window.waitForEmptyEventQueue();
// Refresh the group state all allow the caller to try again.
await maybeUpdateGroup({ conversation, force: true }); await maybeUpdateGroup({ conversation, force: true });
return { state: null, didRefreshGroupState: true }; return { state: null, didRefreshGroupState: true };
} }
return { state: groupSendEndorsementState, didRefreshGroupState: false }; return { state, didRefreshGroupState: false };
} }