Keep profileKey in situations where it didn't work; still fail over

This commit is contained in:
automated-signal 2024-12-10 12:22:49 -06:00 committed by GitHub
parent a871a00dcc
commit 0e124b9e0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 55 additions and 95 deletions

View file

@ -2341,6 +2341,9 @@ export class ConversationModel extends window.Backbone
async addMessageRequestResponseEventMessage( async addMessageRequestResponseEventMessage(
event: MessageRequestResponseEvent event: MessageRequestResponseEvent
): Promise<void> { ): Promise<void> {
const idForLogging = getConversationIdForLogging(this.attributes);
log.info(`addMessageRequestResponseEventMessage/${idForLogging}: ${event}`);
const timestamp = Date.now(); const timestamp = Date.now();
const lastMessageTimestamp = const lastMessageTimestamp =
// Fallback to `timestamp` since `lastMessageReceivedAtMs` is new // Fallback to `timestamp` since `lastMessageReceivedAtMs` is new
@ -5018,7 +5021,7 @@ export class ConversationModel extends window.Backbone
} }
async setProfileKey( async setProfileKey(
profileKey: string | undefined, profileKey: string,
{ {
viaStorageServiceSync = false, viaStorageServiceSync = false,
reason, reason,
@ -5028,6 +5031,10 @@ export class ConversationModel extends window.Backbone
profileKey == null || profileKey.length > 0, profileKey == null || profileKey.length > 0,
'setProfileKey: Profile key cannot be an empty string' 'setProfileKey: Profile key cannot be an empty string'
); );
if (profileKey === undefined) {
log.warn('setProfileKey: Refusing to set an undefined profileKey');
return false;
}
const oldProfileKey = this.get('profileKey'); const oldProfileKey = this.get('profileKey');
@ -5077,7 +5084,7 @@ export class ConversationModel extends window.Backbone
// If our profile key was cleared above, we don't tell our linked devices about it. // If our profile key was cleared above, we don't tell our linked devices about it.
// We want linked devices to tell us what it should be, instead of telling them to // We want linked devices to tell us what it should be, instead of telling them to
// erase their local value. // erase their local value.
if (!viaStorageServiceSync && profileKey) { if (!viaStorageServiceSync) {
this.captureChange('profileKey'); this.captureChange('profileKey');
} }
@ -5151,10 +5158,8 @@ export class ConversationModel extends window.Backbone
const profileKeyVersion = deriveProfileKeyVersion(profileKey, serviceId); const profileKeyVersion = deriveProfileKeyVersion(profileKey, serviceId);
if (!profileKeyVersion) { if (!profileKeyVersion) {
log.warn( log.warn(
'deriveProfileKeyVersion: Failed to derive profile key version, ' + 'deriveProfileKeyVersion: Failed to derive profile key version, return nothing.'
'clearing profile key.'
); );
void this.setProfileKey(undefined, { reason: 'deriveProfileKeyVersion' });
return; return;
} }
@ -5189,32 +5194,6 @@ export class ConversationModel extends window.Backbone
await DataWriter.updateConversation(this.attributes); await DataWriter.updateConversation(this.attributes);
} }
async removeLastProfile(
oldValue: ConversationLastProfileType | undefined
): Promise<void> {
// Atomic updates only
if (this.get('lastProfile') !== oldValue) {
return;
}
log.warn(
'ConversationModel.removeLastProfile: called for',
this.idForLogging()
);
this.set({
lastProfile: undefined,
// We don't have any knowledge of profile anymore. Drop all associated
// data.
about: undefined,
aboutEmoji: undefined,
profileAvatar: undefined,
});
await DataWriter.updateConversation(this.attributes);
}
hasMember(serviceId: ServiceIdString): boolean { hasMember(serviceId: ServiceIdString): boolean {
const members = this.getMembers(); const members = this.getMembers();

View file

@ -292,11 +292,13 @@ async function buildProfileFetchOptions({
lastProfile, lastProfile,
clientZkProfileCipher, clientZkProfileCipher,
groupId, groupId,
options,
}: { }: {
conversation: ConversationModel; conversation: ConversationModel;
lastProfile: ConversationLastProfileType | null; lastProfile: ConversationLastProfileType | null;
clientZkProfileCipher: ClientZkProfileOperations; clientZkProfileCipher: ClientZkProfileOperations;
groupId: string | null; groupId: string | null;
options: { ignoreProfileKey: boolean; ignoreGroupSendToken: boolean };
}): Promise<ProfileFetchOptions.Auth | ProfileFetchOptions.Unauth> { }): Promise<ProfileFetchOptions.Auth | ProfileFetchOptions.Unauth> {
const logId = `buildGetProfileOptions(${conversation.idForLogging()})`; const logId = `buildGetProfileOptions(${conversation.idForLogging()})`;
@ -310,12 +312,12 @@ async function buildProfileFetchOptions({
const accessKey = conversation.get('accessKey'); const accessKey = conversation.get('accessKey');
const serviceId = conversation.getCheckedServiceId('getProfile'); const serviceId = conversation.getCheckedServiceId('getProfile');
if (profileKey) { if (
strictAssert( profileKey &&
profileKeyVersion != null && accessKey != null, profileKeyVersion &&
`${logId}: profileKeyVersion and accessKey are derived from profileKey` accessKey &&
); !options.ignoreProfileKey
) {
if (!conversation.hasProfileKeyCredentialExpired()) { if (!conversation.hasProfileKeyCredentialExpired()) {
log.info(`${logId}: using unexpired profile key credential`); log.info(`${logId}: using unexpired profile key credential`);
return { return {
@ -351,15 +353,12 @@ async function buildProfileFetchOptions({
}; };
} }
strictAssert( // If we're ignoring profileKey, try getting the versioned profile with lastProfile.
accessKey == null, // Note: No access key, since this is almost guaranteed not to be their current profile.
`${logId}: accessKey have to be absent because there is no profileKey` // Also, we can't try the group send token here because the versioned profile can't be
); // decrypted without an up to date profile key.
// If we have a `lastProfile`, try getting the versioned profile with auth.
// Note: We can't try the group send token here because the versioned profile
// can't be decrypted without an up to date profile key.
if ( if (
options.ignoreProfileKey &&
lastProfile != null && lastProfile != null &&
lastProfile.profileKey != null && lastProfile.profileKey != null &&
lastProfile.profileKeyVersion != null lastProfile.profileKeyVersion != null
@ -379,7 +378,7 @@ async function buildProfileFetchOptions({
} }
// Fallback to group send tokens for unversioned profiles // Fallback to group send tokens for unversioned profiles
if (groupId != null) { if (groupId != null && !options.ignoreGroupSendToken) {
log.info(`${logId}: fetching group endorsements`); log.info(`${logId}: fetching group endorsements`);
let result = await maybeCreateGroupSendEndorsementState(groupId, false); let result = await maybeCreateGroupSendEndorsementState(groupId, false);
@ -456,7 +455,14 @@ function getFetchOptionsLabel(
async function doGetProfile( async function doGetProfile(
c: ConversationModel, c: ConversationModel,
groupId: string | null groupId: string | null,
{
ignoreProfileKey,
ignoreGroupSendToken,
}: {
ignoreProfileKey: boolean;
ignoreGroupSendToken: boolean;
} = { ignoreProfileKey: false, ignoreGroupSendToken: false }
): Promise<void> { ): Promise<void> {
const logId = groupId const logId = groupId
? `getProfile(${c.idForLogging()} in groupv2(${groupId}))` ? `getProfile(${c.idForLogging()} in groupv2(${groupId}))`
@ -482,10 +488,8 @@ async function doGetProfile(
const serviceId = c.getCheckedServiceId('getProfile'); const serviceId = c.getCheckedServiceId('getProfile');
// Step #: Grab the profile key and version we last were successful decrypting with // Step #: Grab the profile key and version we last were successful decrypting with.
// `lastProfile` is saved at the end of `doGetProfile` after successfully decrypting. // We'll use it in case we've failed to fetch with our `profileKey`.
// `lastProfile` is used in case the `profileKey` was cleared because of a 401/403.
// `lastProfile` is cleared when we get a 404 fetching a profile.
const lastProfile = c.get('lastProfile'); const lastProfile = c.get('lastProfile');
// Step #: Build the request options we will use for fetching and decrypting the profile // Step #: Build the request options we will use for fetching and decrypting the profile
@ -494,6 +498,10 @@ async function doGetProfile(
lastProfile: lastProfile ?? null, lastProfile: lastProfile ?? null,
clientZkProfileCipher, clientZkProfileCipher,
groupId, groupId,
options: {
ignoreProfileKey,
ignoreGroupSendToken,
},
}); });
const { request } = options; const { request } = options;
@ -523,43 +531,30 @@ async function doGetProfile(
// Fallback from failed unauth (access key) request // Fallback from failed unauth (access key) request
if (request.accessKey != null) { if (request.accessKey != null) {
log.warn( log.warn(
`${logId}: Got ${error.code} when using access key, removing profileKey and retrying` `${logId}: Got ${error.code} when using access key, failing over to lastProfile`
); );
await c.setProfileKey(undefined, {
reason: 'doGetProfile/accessKey/401+403',
});
// Retry fetch using last known profileKeyVersion or fetch // Record that the accessKey we have in the conversation is invalid
// unversioned profile. const sealedSender = c.get('sealedSender');
return doGetProfile(c, groupId); if (sealedSender !== SEALED_SENDER.DISABLED) {
c.set('sealedSender', SEALED_SENDER.DISABLED);
}
// Retry fetch using last known profileKey or fetch unversioned profile.
return doGetProfile(c, groupId, {
ignoreProfileKey: true,
ignoreGroupSendToken,
});
} }
// Fallback from failed unauth (group send token) request // Fallback from failed unauth (group send token) request
if (request.groupSendToken != null) { if (request.groupSendToken != null) {
log.warn(`${logId}: Got ${error.code} when using group send token`); log.warn(`${logId}: Got ${error.code} when using group send token`);
return doGetProfile(c, null); return doGetProfile(c, null, {
} ignoreProfileKey,
} ignoreGroupSendToken: true,
// Step #: Record if the accessKey we have in the conversation is valid
const sealedSender = c.get('sealedSender');
if (
sealedSender === SEALED_SENDER.ENABLED ||
sealedSender === SEALED_SENDER.UNRESTRICTED
) {
if (!isMe(c.attributes)) {
log.warn(
`${logId}: Got ${error.code} when using accessKey, removing profileKey`
);
await c.setProfileKey(undefined, {
reason: 'doGetProfile/accessKey/401+403',
}); });
} }
} else if (sealedSender === SEALED_SENDER.UNKNOWN) {
log.warn(
`${logId}: Got ${error.code} fetching profile, setting sealedSender = DISABLED`
);
c.set('sealedSender', SEALED_SENDER.DISABLED);
} }
// TODO: Is it safe to ignore these errors? // TODO: Is it safe to ignore these errors?
@ -571,10 +566,8 @@ async function doGetProfile(
log.info(`${logId}: Profile not found`); log.info(`${logId}: Profile not found`);
c.set('profileLastFetchedAt', Date.now()); c.set('profileLastFetchedAt', Date.now());
// Note: Writes to DB:
await c.removeLastProfile(lastProfile);
if (!isVersioned) { if (!isVersioned || ignoreProfileKey) {
log.info(`${logId}: Marking conversation unregistered`); log.info(`${logId}: Marking conversation unregistered`);
c.setUnregistered(); c.setUnregistered();
} }

View file

@ -108,19 +108,7 @@ function processError(error: unknown): void {
'private' 'private'
); );
if (error.code === 401 || error.code === 403) { if (error.code === 401 || error.code === 403) {
if ( if (conversation.get('sealedSender') !== SEALED_SENDER.DISABLED) {
conversation.get('sealedSender') === SEALED_SENDER.ENABLED ||
conversation.get('sealedSender') === SEALED_SENDER.UNRESTRICTED
) {
log.warn(
`handleMessageSend: Got 401/403 for ${conversation.idForLogging()}, removing profile key`
);
void conversation.setProfileKey(undefined, {
reason: 'handleMessageSend/processError',
});
}
if (conversation.get('sealedSender') === SEALED_SENDER.UNKNOWN) {
log.warn( log.warn(
`handleMessageSend: Got 401/403 for ${conversation.idForLogging()}, setting sealedSender = DISABLED` `handleMessageSend: Got 401/403 for ${conversation.idForLogging()}, setting sealedSender = DISABLED`
); );