diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts index 326818caf..88e0c475b 100644 --- a/ts/model-types.d.ts +++ b/ts/model-types.d.ts @@ -225,6 +225,11 @@ export type MessageAttributesType = { export type ConversationAttributesTypeType = 'private' | 'group'; +export type ConversationLastProfileType = Readonly<{ + profileKey: string; + profileKeyVersion: string; +}>; + export type ConversationAttributesType = { accessKey?: string | null; addedBy?: string; @@ -262,7 +267,7 @@ export type ConversationAttributesType = { path: string; }; profileKeyCredential?: string | null; - profileKeyVersion?: string | null; + lastProfile?: ConversationLastProfileType; quotedMessageId?: string | null; sealedSender?: unknown; sentMessageCount: number; diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index 193be8a0f..8a3b5b31e 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -8,6 +8,7 @@ import PQueue from 'p-queue'; import type { ConversationAttributesType, + ConversationLastProfileType, ConversationModelCollectionType, LastMessageStatus, MessageAttributesType, @@ -145,6 +146,7 @@ const SEND_REPORTING_THRESHOLD_MS = 25; const MESSAGE_LOAD_CHUNK_SIZE = 30; const ATTRIBUTES_THAT_DONT_INVALIDATE_PROPS_CACHE = new Set([ + 'lastProfile', 'profileLastFetchedAt', 'needsStorageServiceSync', 'storageID', @@ -4575,20 +4577,16 @@ export class ConversationModel extends window.Backbone return this._activeProfileFetch; } - async setEncryptedProfileName(encryptedName: string): Promise { + async setEncryptedProfileName( + encryptedName: string, + decryptionKey: Uint8Array + ): Promise { if (!encryptedName) { return; } - const key = this.get('profileKey'); - if (!key) { - return; - } - - // decode - const keyBuffer = Bytes.fromBase64(key); // decrypt - const { given, family } = decryptProfileName(encryptedName, keyBuffer); + const { given, family } = decryptProfileName(encryptedName, decryptionKey); // encode const profileName = given ? Bytes.toString(given) : undefined; @@ -4617,7 +4615,10 @@ export class ConversationModel extends window.Backbone } } - async setProfileAvatar(avatarPath: undefined | null | string): Promise { + async setProfileAvatar( + avatarPath: undefined | null | string, + decryptionKey: Uint8Array + ): Promise { if (isMe(this.attributes)) { if (avatarPath) { window.storage.put('avatarUrl', avatarPath); @@ -4632,14 +4633,9 @@ export class ConversationModel extends window.Backbone } const avatar = await window.textsecure.messaging.getAvatar(avatarPath); - const key = this.get('profileKey'); - if (!key) { - return; - } - const keyBuffer = Bytes.fromBase64(key); // decrypt - const decrypted = decryptProfile(avatar, keyBuffer); + const decrypted = decryptProfile(avatar, decryptionKey); // update the conversation avatar only if hash differs if (decrypted) { @@ -4666,10 +4662,6 @@ export class ConversationModel extends window.Backbone `Setting sealedSender to UNKNOWN for conversation ${this.idForLogging()}` ); this.set({ - about: undefined, - aboutEmoji: undefined, - profileAvatar: undefined, - profileKeyVersion: undefined, profileKeyCredential: null, accessKey: null, sealedSender: SEALED_SENDER.UNKNOWN, @@ -4685,10 +4677,7 @@ export class ConversationModel extends window.Backbone this.captureChange('profileKey'); } - await Promise.all([ - this.deriveAccessKeyIfNeeded(), - this.deriveProfileKeyVersionIfNeeded(), - ]); + this.deriveAccessKeyIfNeeded(); // We will update the conversation during storage service sync if (!viaStorageServiceSync) { @@ -4700,7 +4689,7 @@ export class ConversationModel extends window.Backbone return false; } - async deriveAccessKeyIfNeeded(): Promise { + deriveAccessKeyIfNeeded(): void { const profileKey = this.get('profileKey'); if (!profileKey) { return; @@ -4715,29 +4704,90 @@ export class ConversationModel extends window.Backbone this.set({ accessKey }); } - async deriveProfileKeyVersionIfNeeded(): Promise { + deriveProfileKeyVersion(): string | undefined { const profileKey = this.get('profileKey'); if (!profileKey) { return; } const uuid = this.get('uuid'); - if (!uuid || this.get('profileKeyVersion')) { + if (!uuid) { return; } + const lastProfile = this.get('lastProfile'); + if (lastProfile?.profileKey === profileKey) { + return lastProfile.profileKeyVersion; + } + const profileKeyVersion = Util.zkgroup.deriveProfileKeyVersion( profileKey, uuid ); if (!profileKeyVersion) { log.warn( - 'deriveProfileKeyVersionIfNeeded: Failed to derive profile key version, clearing profile key.' + 'deriveProfileKeyVersion: Failed to derive profile key version, ' + + 'clearing profile key.' ); this.setProfileKey(undefined); + return; } - this.set({ profileKeyVersion }); + return profileKeyVersion; + } + + async updateLastProfile( + oldValue: ConversationLastProfileType | undefined, + { profileKey, profileKeyVersion }: ConversationLastProfileType + ): Promise { + const lastProfile = this.get('lastProfile'); + + // Atomic updates only + if (lastProfile !== oldValue) { + return; + } + + if ( + lastProfile?.profileKey === profileKey && + lastProfile?.profileKeyVersion === profileKeyVersion + ) { + return; + } + + log.warn( + 'ConversationModel.updateLastProfile: updating for', + this.idForLogging() + ); + + this.set({ lastProfile: { profileKey, profileKeyVersion } }); + + await window.Signal.Data.updateConversation(this.attributes); + } + + async removeLastProfile( + oldValue: ConversationLastProfileType | undefined + ): Promise { + // 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 window.Signal.Data.updateConversation(this.attributes); } hasMember(identifier: string): boolean { diff --git a/ts/services/storageRecordOps.ts b/ts/services/storageRecordOps.ts index 35a0a68db..73197a5a3 100644 --- a/ts/services/storageRecordOps.ts +++ b/ts/services/storageRecordOps.ts @@ -1140,16 +1140,16 @@ export async function mergeAccountRecord( }); let needsProfileFetch = false; - if (accountRecord.profileKey && accountRecord.profileKey.length > 0) { + if (profileKey && profileKey.length > 0) { needsProfileFetch = await conversation.setProfileKey( - Bytes.toBase64(accountRecord.profileKey), + Bytes.toBase64(profileKey), { viaStorageServiceSync: true } ); - } - if (avatarUrl) { - await conversation.setProfileAvatar(avatarUrl); - window.storage.put('avatarUrl', avatarUrl); + if (avatarUrl) { + await conversation.setProfileAvatar(avatarUrl, profileKey); + window.storage.put('avatarUrl', avatarUrl); + } } const { hasConflict, details: extraDetails } = doesRecordHavePendingChanges( diff --git a/ts/test-electron/routineProfileRefresh_test.ts b/ts/test-electron/routineProfileRefresh_test.ts index 1ecb8cf9e..3e76fd31a 100644 --- a/ts/test-electron/routineProfileRefresh_test.ts +++ b/ts/test-electron/routineProfileRefresh_test.ts @@ -44,7 +44,6 @@ describe('routineProfileRefresh', () => { muteExpiresAt: 0, profileAvatar: undefined, profileKeyCredential: UUID.generate().toString(), - profileKeyVersion: '', profileSharing: true, quotedMessageId: null, sealedSender: 1, diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts index 6a8c23e1f..4af8a2a1b 100644 --- a/ts/textsecure/SendMessage.ts +++ b/ts/textsecure/SendMessage.ts @@ -29,6 +29,8 @@ import type { UUID, UUIDStringType } from '../types/UUID'; import type { ChallengeType, GetGroupLogOptionsType, + GetProfileOptionsType, + GetProfileUnauthOptionsType, GroupCredentialsType, GroupLogResponseType, MultiRecipient200ResponseType, @@ -1996,21 +1998,10 @@ export default class MessageSender { async getProfile( uuid: UUID, - options: Readonly<{ - accessKey?: string; - profileKeyVersion: string; - profileKeyCredentialRequest?: string; - userLanguages: ReadonlyArray; - }> + options: GetProfileOptionsType | GetProfileUnauthOptionsType ): ReturnType { - const { accessKey } = options; - - if (accessKey) { - const unauthOptions = { - ...options, - accessKey, - }; - return this.server.getProfileUnauth(uuid.toString(), unauthOptions); + if (options.accessKey !== undefined) { + return this.server.getProfileUnauth(uuid.toString(), options); } return this.server.getProfile(uuid.toString(), options); diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index b4d92cd0d..3f7753c01 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -778,6 +778,32 @@ export type GetUuidsForE164sV2OptionsType = Readonly<{ accessKeys: ReadonlyArray; }>; +type GetProfileCommonOptionsType = Readonly< + { + userLanguages: ReadonlyArray; + credentialType?: 'pni' | 'profileKey'; + } & ( + | { + profileKeyVersion?: undefined; + profileKeyCredentialRequest?: undefined; + } + | { + profileKeyVersion: string; + profileKeyCredentialRequest?: string; + } + ) +>; + +export type GetProfileOptionsType = GetProfileCommonOptionsType & + Readonly<{ + accessKey?: undefined; + }>; + +export type GetProfileUnauthOptionsType = GetProfileCommonOptionsType & + Readonly<{ + accessKey: string; + }>; + export type WebAPIType = { startRegistration(): unknown; finishRegistration(baton: unknown): void; @@ -829,22 +855,12 @@ export type WebAPIType = { getMyKeys: (uuidKind: UUIDKind) => Promise; getProfile: ( identifier: string, - options: { - profileKeyVersion: string; - profileKeyCredentialRequest?: string; - userLanguages: ReadonlyArray; - credentialType?: 'pni' | 'profileKey'; - } + options: GetProfileOptionsType ) => Promise; getProfileForUsername: (username: string) => Promise; getProfileUnauth: ( identifier: string, - options: { - accessKey: string; - profileKeyVersion: string; - profileKeyCredentialRequest?: string; - userLanguages: ReadonlyArray; - } + options: GetProfileUnauthOptionsType ) => Promise; getBadgeImageFile: (imageUrl: string) => Promise; getProvisioningResource: ( @@ -1482,14 +1498,25 @@ export function initialize({ function getProfileUrl( identifier: string, - profileKeyVersion: string, - profileKeyCredentialRequest?: string, - credentialType: 'pni' | 'profileKey' = 'profileKey' + { + profileKeyVersion, + profileKeyCredentialRequest, + credentialType = 'profileKey', + }: GetProfileCommonOptionsType ) { - let profileUrl = `/${identifier}/${profileKeyVersion}`; - - if (profileKeyCredentialRequest) { - profileUrl += `/${profileKeyCredentialRequest}?credentialType=${credentialType}`; + let profileUrl = `/${identifier}`; + if (profileKeyVersion !== undefined) { + profileUrl += `/${profileKeyVersion}`; + if (profileKeyCredentialRequest !== undefined) { + profileUrl += + `/${profileKeyCredentialRequest}` + + `?credentialType=${credentialType}`; + } + } else { + strictAssert( + profileKeyCredentialRequest === undefined, + 'getProfileUrl called without version, but with request' + ); } return profileUrl; @@ -1497,29 +1524,15 @@ export function initialize({ async function getProfile( identifier: string, - options: { - profileKeyVersion: string; - profileKeyCredentialRequest?: string; - userLanguages: ReadonlyArray; - credentialType?: 'pni' | 'profileKey'; - } + options: GetProfileOptionsType ) { - const { - profileKeyVersion, - profileKeyCredentialRequest, - userLanguages, - credentialType = 'profileKey', - } = options; + const { profileKeyVersion, profileKeyCredentialRequest, userLanguages } = + options; return (await _ajax({ call: 'profile', httpType: 'GET', - urlParameters: getProfileUrl( - identifier, - profileKeyVersion, - profileKeyCredentialRequest, - credentialType - ), + urlParameters: getProfileUrl(identifier, options), headers: { 'Accept-Language': formatAcceptLanguageHeader(userLanguages), }, @@ -1561,12 +1574,7 @@ export function initialize({ async function getProfileUnauth( identifier: string, - options: { - accessKey: string; - profileKeyVersion: string; - profileKeyCredentialRequest?: string; - userLanguages: ReadonlyArray; - } + options: GetProfileUnauthOptionsType ) { const { accessKey, @@ -1578,11 +1586,7 @@ export function initialize({ return (await _ajax({ call: 'profile', httpType: 'GET', - urlParameters: getProfileUrl( - identifier, - profileKeyVersion, - profileKeyCredentialRequest - ), + urlParameters: getProfileUrl(identifier, options), headers: { 'Accept-Language': formatAcceptLanguageHeader(userLanguages), }, diff --git a/ts/util/getProfile.ts b/ts/util/getProfile.ts index 5a0507019..8c4ca6a94 100644 --- a/ts/util/getProfile.ts +++ b/ts/util/getProfile.ts @@ -3,6 +3,12 @@ import type { ProfileKeyCredentialRequestContext } from '@signalapp/signal-client/zkgroup'; import { SEALED_SENDER } from '../types/SealedSender'; +import * as Errors from '../types/errors'; +import type { + GetProfileOptionsType, + GetProfileUnauthOptionsType, +} from '../textsecure/WebAPI'; +import { HTTPError } from '../textsecure/Errors'; import { Address } from '../types/Address'; import { QualifiedAddress } from '../types/QualifiedAddress'; import * as Bytes from '../Bytes'; @@ -12,36 +18,26 @@ import { getClientZkProfileOperations, handleProfileKeyCredential, } from './zkgroup'; -import { getSendOptions } from './getSendOptions'; import { isMe } from './whatTypeOfConversation'; +import type { ConversationModel } from '../models/conversations'; import * as log from '../logging/log'; import { getUserLanguages } from './userLanguages'; import { parseBadgesFromServer } from '../badges/parseBadgesFromServer'; +import { strictAssert } from './assert'; -export async function getProfile( - providedUuid?: string, - providedE164?: string -): Promise { - if (!window.textsecure.messaging) { - throw new Error( - 'Conversation.getProfile: window.textsecure.messaging not available' - ); - } +async function doGetProfile(c: ConversationModel): Promise { + const idForLogging = c.idForLogging(); + const { messaging } = window.textsecure; + strictAssert( + messaging, + 'getProfile: window.textsecure.messaging not available' + ); const { updatesUrl } = window.SignalContext.config; - if (typeof updatesUrl !== 'string') { - throw new Error('getProfile expected updatesUrl to be a defined string'); - } - - const id = window.ConversationController.ensureContactIds({ - uuid: providedUuid, - e164: providedE164, - }); - const c = window.ConversationController.get(id); - if (!c) { - log.error('getProfile: failed to find conversation; doing nothing'); - return; - } + strictAssert( + typeof updatesUrl === 'string', + 'getProfile: expected updatesUrl to be a defined string' + ); const clientZkProfileCipher = getClientZkProfileOperations( window.getServerPublicParams() @@ -54,27 +50,40 @@ export async function getProfile( let profile; - try { - await Promise.all([ - c.deriveAccessKeyIfNeeded(), - c.deriveProfileKeyVersionIfNeeded(), - ]); + c.deriveAccessKeyIfNeeded(); - const profileKey = c.get('profileKey'); - const uuid = c.getCheckedUuid('getProfile'); - const profileKeyVersionHex = c.get('profileKeyVersion'); - if (!profileKeyVersionHex) { - throw new Error('No profile key version available'); - } - const existingProfileKeyCredential = c.get('profileKeyCredential'); + const profileKey = c.get('profileKey'); + const profileKeyVersion = c.deriveProfileKeyVersion(); + const uuid = c.getCheckedUuid('getProfile'); + const existingProfileKeyCredential = c.get('profileKeyCredential'); + const lastProfile = c.get('lastProfile'); - let profileKeyCredentialRequestHex: undefined | string; - let profileCredentialRequestContext: - | undefined - | ProfileKeyCredentialRequestContext; + let profileCredentialRequestContext: + | undefined + | ProfileKeyCredentialRequestContext; - if (profileKey && profileKeyVersionHex && !existingProfileKeyCredential) { - log.info('Generating request...'); + let getProfileOptions: GetProfileOptionsType | GetProfileUnauthOptionsType; + + let accessKey = c.get('accessKey'); + if (profileKey) { + strictAssert( + profileKeyVersion && accessKey, + 'profileKeyVersion and accessKey are derived from profileKey' + ); + + if (existingProfileKeyCredential) { + getProfileOptions = { + accessKey, + profileKeyVersion, + userLanguages, + }; + } else { + log.info( + 'getProfile: generating profile key credential request for ' + + `conversation ${idForLogging}` + ); + + let profileKeyCredentialRequestHex: undefined | string; ({ requestHex: profileKeyCredentialRequestHex, context: profileCredentialRequestContext, @@ -83,40 +92,76 @@ export async function getProfile( uuid.toString(), profileKey )); + + getProfileOptions = { + accessKey, + userLanguages, + profileKeyVersion, + profileKeyCredentialRequest: profileKeyCredentialRequestHex, + }; } + } else { + strictAssert( + !accessKey, + 'accessKey have to be absent because there is no profileKey' + ); - const { sendMetadata = {} } = await getSendOptions(c.attributes); - const getInfo = sendMetadata[uuid.toString()] || {}; + if (lastProfile?.profileKeyVersion) { + getProfileOptions = { + userLanguages, + profileKeyVersion: lastProfile.profileKeyVersion, + }; + } else { + getProfileOptions = { userLanguages }; + } + } - if (getInfo.accessKey) { + const isVersioned = Boolean(getProfileOptions.profileKeyVersion); + log.info( + `getProfile: getting ${isVersioned ? 'versioned' : 'unversioned'} ` + + `profile for conversation ${idForLogging}` + ); + + try { + if (getProfileOptions.accessKey) { try { - profile = await window.textsecure.messaging.getProfile(uuid, { - accessKey: getInfo.accessKey, - profileKeyVersion: profileKeyVersionHex, - profileKeyCredentialRequest: profileKeyCredentialRequestHex, - userLanguages, - }); + profile = await messaging.getProfile(uuid, getProfileOptions); } catch (error) { - if (error.code === 401 || error.code === 403) { - log.info( - `Setting sealedSender to DISABLED for conversation ${c.idForLogging()}` - ); - c.set({ sealedSender: SEALED_SENDER.DISABLED }); - profile = await window.textsecure.messaging.getProfile(uuid, { - profileKeyVersion: profileKeyVersionHex, - profileKeyCredentialRequest: profileKeyCredentialRequestHex, - userLanguages, - }); - } else { + if (!(error instanceof HTTPError)) { throw error; } + if (error.code === 401 || error.code === 403) { + await c.setProfileKey(undefined); + + // Retry fetch using last known profileKeyVersion or fetch + // unversioned profile. + return doGetProfile(c); + } + + if (error.code === 404) { + await c.removeLastProfile(lastProfile); + } + + throw error; } } else { - profile = await window.textsecure.messaging.getProfile(uuid, { - profileKeyVersion: profileKeyVersionHex, - profileKeyCredentialRequest: profileKeyCredentialRequestHex, - userLanguages, - }); + try { + // We won't get the credential, but lets either fetch: + // - a versioned profile using last known profileKeyVersion + // - some basic profile information (capabilities, badges, etc). + profile = await messaging.getProfile(uuid, getProfileOptions); + } catch (error) { + if (error instanceof HTTPError && error.code === 404) { + log.info(`getProfile: failed to find a profile for ${idForLogging}`); + + await c.removeLastProfile(lastProfile); + if (!isVersioned) { + log.info(`getProfile: marking ${idForLogging} as unregistered`); + c.setUnregistered(); + } + } + throw error; + } } if (profile.identityKey) { @@ -136,10 +181,14 @@ export async function getProfile( } } - const accessKey = c.get('accessKey'); + // Update accessKey to prevent race conditions. Since we run asynchronous + // requests above - it is possible that someone updates or erases + // the profile key from under us. + accessKey = c.get('accessKey'); + if (profile.unrestrictedUnidentifiedAccess && profile.unidentifiedAccess) { log.info( - `Setting sealedSender to UNRESTRICTED for conversation ${c.idForLogging()}` + `getProfile: setting sealedSender to UNRESTRICTED for conversation ${idForLogging}` ); c.set({ sealedSender: SEALED_SENDER.UNRESTRICTED, @@ -152,14 +201,14 @@ export async function getProfile( if (haveCorrectKey) { log.info( - `Setting sealedSender to ENABLED for conversation ${c.idForLogging()}` + `getProfile: setting sealedSender to ENABLED for conversation ${idForLogging}` ); c.set({ sealedSender: SEALED_SENDER.ENABLED, }); } else { - log.info( - `Setting sealedSender to DISABLED for conversation ${c.idForLogging()}` + log.warn( + `getProfile: setting sealedSender to DISABLED for conversation ${idForLogging}` ); c.set({ sealedSender: SEALED_SENDER.DISABLED, @@ -167,20 +216,22 @@ export async function getProfile( } } else { log.info( - `Setting sealedSender to DISABLED for conversation ${c.idForLogging()}` + `getProfile: setting sealedSender to DISABLED for conversation ${idForLogging}` ); c.set({ sealedSender: SEALED_SENDER.DISABLED, }); } + const rawDecryptionKey = c.get('profileKey') || lastProfile?.profileKey; + const decryptionKey = rawDecryptionKey + ? Bytes.fromBase64(rawDecryptionKey) + : undefined; if (profile.about) { - const key = c.get('profileKey'); - if (key) { - const keyBuffer = Bytes.fromBase64(key); + if (decryptionKey) { const decrypted = decryptProfile( Bytes.fromBase64(profile.about), - keyBuffer + decryptionKey ); c.set('about', Bytes.toString(trimForDisplay(decrypted))); } @@ -189,12 +240,10 @@ export async function getProfile( } if (profile.aboutEmoji) { - const key = c.get('profileKey'); - if (key) { - const keyBuffer = Bytes.fromBase64(key); + if (decryptionKey) { const decrypted = decryptProfile( Bytes.fromBase64(profile.aboutEmoji), - keyBuffer + decryptionKey ); c.set('aboutEmoji', Bytes.toString(trimForDisplay(decrypted))); } @@ -243,7 +292,11 @@ export async function getProfile( } } } catch (error) { - switch (error?.code) { + if (!(error instanceof HTTPError)) { + throw error; + } + + switch (error.code) { case 401: case 403: if ( @@ -251,46 +304,49 @@ export async function getProfile( c.get('sealedSender') === SEALED_SENDER.UNRESTRICTED ) { log.warn( - `getProfile: Got 401/403 when using accessKey for ${c.idForLogging()}, removing profileKey` + `getProfile: Got 401/403 when using accessKey for ${idForLogging}, removing profileKey` ); c.setProfileKey(undefined); } if (c.get('sealedSender') === SEALED_SENDER.UNKNOWN) { log.warn( - `getProfile: Got 401/403 when using accessKey for ${c.idForLogging()}, setting sealedSender = DISABLED` + `getProfile: Got 401/403 when using accessKey for ${idForLogging}, setting sealedSender = DISABLED` ); c.set('sealedSender', SEALED_SENDER.DISABLED); } return; - case 404: - log.info( - `getProfile: failed to find a profile for ${c.idForLogging()}` - ); - c.setUnregistered(); - return; default: log.warn( 'getProfile failure:', - c.idForLogging(), - error && error.stack ? error.stack : error + idForLogging, + Errors.toLogFormat(error) ); return; } } + const decryptionKeyString = profileKey || lastProfile?.profileKey; + const decryptionKey = decryptionKeyString + ? Bytes.fromBase64(decryptionKeyString) + : undefined; + + let isSuccessfullyDecrypted = true; if (profile.name) { - try { - await c.setEncryptedProfileName(profile.name); - } catch (error) { - log.warn( - 'getProfile decryption failure:', - c.idForLogging(), - error && error.stack ? error.stack : error - ); - await c.set({ - profileName: undefined, - profileFamilyName: undefined, - }); + if (decryptionKey) { + try { + await c.setEncryptedProfileName(profile.name, decryptionKey); + } catch (error) { + log.warn( + 'getProfile decryption failure:', + idForLogging, + Errors.toLogFormat(error) + ); + isSuccessfullyDecrypted = false; + await c.set({ + profileName: undefined, + profileFamilyName: undefined, + }); + } } } else { c.set({ @@ -300,17 +356,58 @@ export async function getProfile( } try { - await c.setProfileAvatar(profile.avatar); + if (decryptionKey) { + await c.setProfileAvatar(profile.avatar, decryptionKey); + } } catch (error) { - if (error.code === 403 || error.code === 404) { - log.info(`Clearing profile avatar for conversation ${c.idForLogging()}`); - c.set({ - profileAvatar: null, - }); + if (error instanceof HTTPError) { + if (error.code === 403 || error.code === 404) { + log.warn( + `getProfile: clearing profile avatar for conversation ${idForLogging}` + ); + c.set({ + profileAvatar: null, + }); + } + } else { + log.warn( + `getProfile: failed to decrypt avatar for conversation ${idForLogging}`, + Errors.toLogFormat(error) + ); + isSuccessfullyDecrypted = false; } } c.set('profileLastFetchedAt', Date.now()); + // After we successfully decrypted - update lastProfile property + if ( + isSuccessfullyDecrypted && + profileKey && + getProfileOptions.profileKeyVersion + ) { + await c.updateLastProfile(lastProfile, { + profileKey, + profileKeyVersion: getProfileOptions.profileKeyVersion, + }); + } + window.Signal.Data.updateConversation(c.attributes); } + +export async function getProfile( + providedUuid?: string, + providedE164?: string +): Promise { + const id = window.ConversationController.ensureContactIds({ + uuid: providedUuid, + e164: providedE164, + }); + const c = window.ConversationController.get(id); + if (!c) { + log.error('getProfile: failed to find conversation; doing nothing'); + return; + } + + await doGetProfile(c); +}