Allow requesting profile without profileKey

This commit is contained in:
Fedor Indutny 2022-03-09 12:23:21 -08:00 committed by GitHub
parent 80e445389f
commit 4a00ea46bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 354 additions and 208 deletions

7
ts/model-types.d.ts vendored
View file

@ -225,6 +225,11 @@ export type MessageAttributesType = {
export type ConversationAttributesTypeType = 'private' | 'group'; export type ConversationAttributesTypeType = 'private' | 'group';
export type ConversationLastProfileType = Readonly<{
profileKey: string;
profileKeyVersion: string;
}>;
export type ConversationAttributesType = { export type ConversationAttributesType = {
accessKey?: string | null; accessKey?: string | null;
addedBy?: string; addedBy?: string;
@ -262,7 +267,7 @@ export type ConversationAttributesType = {
path: string; path: string;
}; };
profileKeyCredential?: string | null; profileKeyCredential?: string | null;
profileKeyVersion?: string | null; lastProfile?: ConversationLastProfileType;
quotedMessageId?: string | null; quotedMessageId?: string | null;
sealedSender?: unknown; sealedSender?: unknown;
sentMessageCount: number; sentMessageCount: number;

View file

@ -8,6 +8,7 @@ import PQueue from 'p-queue';
import type { import type {
ConversationAttributesType, ConversationAttributesType,
ConversationLastProfileType,
ConversationModelCollectionType, ConversationModelCollectionType,
LastMessageStatus, LastMessageStatus,
MessageAttributesType, MessageAttributesType,
@ -145,6 +146,7 @@ const SEND_REPORTING_THRESHOLD_MS = 25;
const MESSAGE_LOAD_CHUNK_SIZE = 30; const MESSAGE_LOAD_CHUNK_SIZE = 30;
const ATTRIBUTES_THAT_DONT_INVALIDATE_PROPS_CACHE = new Set([ const ATTRIBUTES_THAT_DONT_INVALIDATE_PROPS_CACHE = new Set([
'lastProfile',
'profileLastFetchedAt', 'profileLastFetchedAt',
'needsStorageServiceSync', 'needsStorageServiceSync',
'storageID', 'storageID',
@ -4575,20 +4577,16 @@ export class ConversationModel extends window.Backbone
return this._activeProfileFetch; return this._activeProfileFetch;
} }
async setEncryptedProfileName(encryptedName: string): Promise<void> { async setEncryptedProfileName(
encryptedName: string,
decryptionKey: Uint8Array
): Promise<void> {
if (!encryptedName) { if (!encryptedName) {
return; return;
} }
const key = this.get('profileKey');
if (!key) {
return;
}
// decode
const keyBuffer = Bytes.fromBase64(key);
// decrypt // decrypt
const { given, family } = decryptProfileName(encryptedName, keyBuffer); const { given, family } = decryptProfileName(encryptedName, decryptionKey);
// encode // encode
const profileName = given ? Bytes.toString(given) : undefined; const profileName = given ? Bytes.toString(given) : undefined;
@ -4617,7 +4615,10 @@ export class ConversationModel extends window.Backbone
} }
} }
async setProfileAvatar(avatarPath: undefined | null | string): Promise<void> { async setProfileAvatar(
avatarPath: undefined | null | string,
decryptionKey: Uint8Array
): Promise<void> {
if (isMe(this.attributes)) { if (isMe(this.attributes)) {
if (avatarPath) { if (avatarPath) {
window.storage.put('avatarUrl', 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 avatar = await window.textsecure.messaging.getAvatar(avatarPath);
const key = this.get('profileKey');
if (!key) {
return;
}
const keyBuffer = Bytes.fromBase64(key);
// decrypt // decrypt
const decrypted = decryptProfile(avatar, keyBuffer); const decrypted = decryptProfile(avatar, decryptionKey);
// update the conversation avatar only if hash differs // update the conversation avatar only if hash differs
if (decrypted) { if (decrypted) {
@ -4666,10 +4662,6 @@ export class ConversationModel extends window.Backbone
`Setting sealedSender to UNKNOWN for conversation ${this.idForLogging()}` `Setting sealedSender to UNKNOWN for conversation ${this.idForLogging()}`
); );
this.set({ this.set({
about: undefined,
aboutEmoji: undefined,
profileAvatar: undefined,
profileKeyVersion: undefined,
profileKeyCredential: null, profileKeyCredential: null,
accessKey: null, accessKey: null,
sealedSender: SEALED_SENDER.UNKNOWN, sealedSender: SEALED_SENDER.UNKNOWN,
@ -4685,10 +4677,7 @@ export class ConversationModel extends window.Backbone
this.captureChange('profileKey'); this.captureChange('profileKey');
} }
await Promise.all([ this.deriveAccessKeyIfNeeded();
this.deriveAccessKeyIfNeeded(),
this.deriveProfileKeyVersionIfNeeded(),
]);
// We will update the conversation during storage service sync // We will update the conversation during storage service sync
if (!viaStorageServiceSync) { if (!viaStorageServiceSync) {
@ -4700,7 +4689,7 @@ export class ConversationModel extends window.Backbone
return false; return false;
} }
async deriveAccessKeyIfNeeded(): Promise<void> { deriveAccessKeyIfNeeded(): void {
const profileKey = this.get('profileKey'); const profileKey = this.get('profileKey');
if (!profileKey) { if (!profileKey) {
return; return;
@ -4715,29 +4704,90 @@ export class ConversationModel extends window.Backbone
this.set({ accessKey }); this.set({ accessKey });
} }
async deriveProfileKeyVersionIfNeeded(): Promise<void> { deriveProfileKeyVersion(): string | undefined {
const profileKey = this.get('profileKey'); const profileKey = this.get('profileKey');
if (!profileKey) { if (!profileKey) {
return; return;
} }
const uuid = this.get('uuid'); const uuid = this.get('uuid');
if (!uuid || this.get('profileKeyVersion')) { if (!uuid) {
return; return;
} }
const lastProfile = this.get('lastProfile');
if (lastProfile?.profileKey === profileKey) {
return lastProfile.profileKeyVersion;
}
const profileKeyVersion = Util.zkgroup.deriveProfileKeyVersion( const profileKeyVersion = Util.zkgroup.deriveProfileKeyVersion(
profileKey, profileKey,
uuid uuid
); );
if (!profileKeyVersion) { if (!profileKeyVersion) {
log.warn( 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); this.setProfileKey(undefined);
return;
} }
this.set({ profileKeyVersion }); return profileKeyVersion;
}
async updateLastProfile(
oldValue: ConversationLastProfileType | undefined,
{ profileKey, profileKeyVersion }: ConversationLastProfileType
): Promise<void> {
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<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 window.Signal.Data.updateConversation(this.attributes);
} }
hasMember(identifier: string): boolean { hasMember(identifier: string): boolean {

View file

@ -1140,16 +1140,16 @@ export async function mergeAccountRecord(
}); });
let needsProfileFetch = false; let needsProfileFetch = false;
if (accountRecord.profileKey && accountRecord.profileKey.length > 0) { if (profileKey && profileKey.length > 0) {
needsProfileFetch = await conversation.setProfileKey( needsProfileFetch = await conversation.setProfileKey(
Bytes.toBase64(accountRecord.profileKey), Bytes.toBase64(profileKey),
{ viaStorageServiceSync: true } { viaStorageServiceSync: true }
); );
}
if (avatarUrl) { if (avatarUrl) {
await conversation.setProfileAvatar(avatarUrl); await conversation.setProfileAvatar(avatarUrl, profileKey);
window.storage.put('avatarUrl', avatarUrl); window.storage.put('avatarUrl', avatarUrl);
}
} }
const { hasConflict, details: extraDetails } = doesRecordHavePendingChanges( const { hasConflict, details: extraDetails } = doesRecordHavePendingChanges(

View file

@ -44,7 +44,6 @@ describe('routineProfileRefresh', () => {
muteExpiresAt: 0, muteExpiresAt: 0,
profileAvatar: undefined, profileAvatar: undefined,
profileKeyCredential: UUID.generate().toString(), profileKeyCredential: UUID.generate().toString(),
profileKeyVersion: '',
profileSharing: true, profileSharing: true,
quotedMessageId: null, quotedMessageId: null,
sealedSender: 1, sealedSender: 1,

View file

@ -29,6 +29,8 @@ import type { UUID, UUIDStringType } from '../types/UUID';
import type { import type {
ChallengeType, ChallengeType,
GetGroupLogOptionsType, GetGroupLogOptionsType,
GetProfileOptionsType,
GetProfileUnauthOptionsType,
GroupCredentialsType, GroupCredentialsType,
GroupLogResponseType, GroupLogResponseType,
MultiRecipient200ResponseType, MultiRecipient200ResponseType,
@ -1996,21 +1998,10 @@ export default class MessageSender {
async getProfile( async getProfile(
uuid: UUID, uuid: UUID,
options: Readonly<{ options: GetProfileOptionsType | GetProfileUnauthOptionsType
accessKey?: string;
profileKeyVersion: string;
profileKeyCredentialRequest?: string;
userLanguages: ReadonlyArray<string>;
}>
): ReturnType<WebAPIType['getProfile']> { ): ReturnType<WebAPIType['getProfile']> {
const { accessKey } = options; if (options.accessKey !== undefined) {
return this.server.getProfileUnauth(uuid.toString(), options);
if (accessKey) {
const unauthOptions = {
...options,
accessKey,
};
return this.server.getProfileUnauth(uuid.toString(), unauthOptions);
} }
return this.server.getProfile(uuid.toString(), options); return this.server.getProfile(uuid.toString(), options);

View file

@ -778,6 +778,32 @@ export type GetUuidsForE164sV2OptionsType = Readonly<{
accessKeys: ReadonlyArray<string>; accessKeys: ReadonlyArray<string>;
}>; }>;
type GetProfileCommonOptionsType = Readonly<
{
userLanguages: ReadonlyArray<string>;
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 = { export type WebAPIType = {
startRegistration(): unknown; startRegistration(): unknown;
finishRegistration(baton: unknown): void; finishRegistration(baton: unknown): void;
@ -829,22 +855,12 @@ export type WebAPIType = {
getMyKeys: (uuidKind: UUIDKind) => Promise<number>; getMyKeys: (uuidKind: UUIDKind) => Promise<number>;
getProfile: ( getProfile: (
identifier: string, identifier: string,
options: { options: GetProfileOptionsType
profileKeyVersion: string;
profileKeyCredentialRequest?: string;
userLanguages: ReadonlyArray<string>;
credentialType?: 'pni' | 'profileKey';
}
) => Promise<ProfileType>; ) => Promise<ProfileType>;
getProfileForUsername: (username: string) => Promise<ProfileType>; getProfileForUsername: (username: string) => Promise<ProfileType>;
getProfileUnauth: ( getProfileUnauth: (
identifier: string, identifier: string,
options: { options: GetProfileUnauthOptionsType
accessKey: string;
profileKeyVersion: string;
profileKeyCredentialRequest?: string;
userLanguages: ReadonlyArray<string>;
}
) => Promise<ProfileType>; ) => Promise<ProfileType>;
getBadgeImageFile: (imageUrl: string) => Promise<Uint8Array>; getBadgeImageFile: (imageUrl: string) => Promise<Uint8Array>;
getProvisioningResource: ( getProvisioningResource: (
@ -1482,14 +1498,25 @@ export function initialize({
function getProfileUrl( function getProfileUrl(
identifier: string, identifier: string,
profileKeyVersion: string, {
profileKeyCredentialRequest?: string, profileKeyVersion,
credentialType: 'pni' | 'profileKey' = 'profileKey' profileKeyCredentialRequest,
credentialType = 'profileKey',
}: GetProfileCommonOptionsType
) { ) {
let profileUrl = `/${identifier}/${profileKeyVersion}`; let profileUrl = `/${identifier}`;
if (profileKeyVersion !== undefined) {
if (profileKeyCredentialRequest) { profileUrl += `/${profileKeyVersion}`;
profileUrl += `/${profileKeyCredentialRequest}?credentialType=${credentialType}`; if (profileKeyCredentialRequest !== undefined) {
profileUrl +=
`/${profileKeyCredentialRequest}` +
`?credentialType=${credentialType}`;
}
} else {
strictAssert(
profileKeyCredentialRequest === undefined,
'getProfileUrl called without version, but with request'
);
} }
return profileUrl; return profileUrl;
@ -1497,29 +1524,15 @@ export function initialize({
async function getProfile( async function getProfile(
identifier: string, identifier: string,
options: { options: GetProfileOptionsType
profileKeyVersion: string;
profileKeyCredentialRequest?: string;
userLanguages: ReadonlyArray<string>;
credentialType?: 'pni' | 'profileKey';
}
) { ) {
const { const { profileKeyVersion, profileKeyCredentialRequest, userLanguages } =
profileKeyVersion, options;
profileKeyCredentialRequest,
userLanguages,
credentialType = 'profileKey',
} = options;
return (await _ajax({ return (await _ajax({
call: 'profile', call: 'profile',
httpType: 'GET', httpType: 'GET',
urlParameters: getProfileUrl( urlParameters: getProfileUrl(identifier, options),
identifier,
profileKeyVersion,
profileKeyCredentialRequest,
credentialType
),
headers: { headers: {
'Accept-Language': formatAcceptLanguageHeader(userLanguages), 'Accept-Language': formatAcceptLanguageHeader(userLanguages),
}, },
@ -1561,12 +1574,7 @@ export function initialize({
async function getProfileUnauth( async function getProfileUnauth(
identifier: string, identifier: string,
options: { options: GetProfileUnauthOptionsType
accessKey: string;
profileKeyVersion: string;
profileKeyCredentialRequest?: string;
userLanguages: ReadonlyArray<string>;
}
) { ) {
const { const {
accessKey, accessKey,
@ -1578,11 +1586,7 @@ export function initialize({
return (await _ajax({ return (await _ajax({
call: 'profile', call: 'profile',
httpType: 'GET', httpType: 'GET',
urlParameters: getProfileUrl( urlParameters: getProfileUrl(identifier, options),
identifier,
profileKeyVersion,
profileKeyCredentialRequest
),
headers: { headers: {
'Accept-Language': formatAcceptLanguageHeader(userLanguages), 'Accept-Language': formatAcceptLanguageHeader(userLanguages),
}, },

View file

@ -3,6 +3,12 @@
import type { ProfileKeyCredentialRequestContext } from '@signalapp/signal-client/zkgroup'; import type { ProfileKeyCredentialRequestContext } from '@signalapp/signal-client/zkgroup';
import { SEALED_SENDER } from '../types/SealedSender'; 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 { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress'; import { QualifiedAddress } from '../types/QualifiedAddress';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
@ -12,36 +18,26 @@ import {
getClientZkProfileOperations, getClientZkProfileOperations,
handleProfileKeyCredential, handleProfileKeyCredential,
} from './zkgroup'; } from './zkgroup';
import { getSendOptions } from './getSendOptions';
import { isMe } from './whatTypeOfConversation'; import { isMe } from './whatTypeOfConversation';
import type { ConversationModel } from '../models/conversations';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { getUserLanguages } from './userLanguages'; import { getUserLanguages } from './userLanguages';
import { parseBadgesFromServer } from '../badges/parseBadgesFromServer'; import { parseBadgesFromServer } from '../badges/parseBadgesFromServer';
import { strictAssert } from './assert';
export async function getProfile( async function doGetProfile(c: ConversationModel): Promise<void> {
providedUuid?: string, const idForLogging = c.idForLogging();
providedE164?: string const { messaging } = window.textsecure;
): Promise<void> { strictAssert(
if (!window.textsecure.messaging) { messaging,
throw new Error( 'getProfile: window.textsecure.messaging not available'
'Conversation.getProfile: window.textsecure.messaging not available' );
);
}
const { updatesUrl } = window.SignalContext.config; const { updatesUrl } = window.SignalContext.config;
if (typeof updatesUrl !== 'string') { strictAssert(
throw new Error('getProfile expected updatesUrl to be a defined string'); typeof updatesUrl === 'string',
} '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;
}
const clientZkProfileCipher = getClientZkProfileOperations( const clientZkProfileCipher = getClientZkProfileOperations(
window.getServerPublicParams() window.getServerPublicParams()
@ -54,27 +50,40 @@ export async function getProfile(
let profile; let profile;
try { c.deriveAccessKeyIfNeeded();
await Promise.all([
c.deriveAccessKeyIfNeeded(),
c.deriveProfileKeyVersionIfNeeded(),
]);
const profileKey = c.get('profileKey'); const profileKey = c.get('profileKey');
const uuid = c.getCheckedUuid('getProfile'); const profileKeyVersion = c.deriveProfileKeyVersion();
const profileKeyVersionHex = c.get('profileKeyVersion'); const uuid = c.getCheckedUuid('getProfile');
if (!profileKeyVersionHex) { const existingProfileKeyCredential = c.get('profileKeyCredential');
throw new Error('No profile key version available'); const lastProfile = c.get('lastProfile');
}
const existingProfileKeyCredential = c.get('profileKeyCredential');
let profileKeyCredentialRequestHex: undefined | string; let profileCredentialRequestContext:
let profileCredentialRequestContext: | undefined
| undefined | ProfileKeyCredentialRequestContext;
| ProfileKeyCredentialRequestContext;
if (profileKey && profileKeyVersionHex && !existingProfileKeyCredential) { let getProfileOptions: GetProfileOptionsType | GetProfileUnauthOptionsType;
log.info('Generating request...');
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, requestHex: profileKeyCredentialRequestHex,
context: profileCredentialRequestContext, context: profileCredentialRequestContext,
@ -83,40 +92,76 @@ export async function getProfile(
uuid.toString(), uuid.toString(),
profileKey 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); if (lastProfile?.profileKeyVersion) {
const getInfo = sendMetadata[uuid.toString()] || {}; 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 { try {
profile = await window.textsecure.messaging.getProfile(uuid, { profile = await messaging.getProfile(uuid, getProfileOptions);
accessKey: getInfo.accessKey,
profileKeyVersion: profileKeyVersionHex,
profileKeyCredentialRequest: profileKeyCredentialRequestHex,
userLanguages,
});
} catch (error) { } catch (error) {
if (error.code === 401 || error.code === 403) { if (!(error instanceof HTTPError)) {
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 {
throw error; 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 { } else {
profile = await window.textsecure.messaging.getProfile(uuid, { try {
profileKeyVersion: profileKeyVersionHex, // We won't get the credential, but lets either fetch:
profileKeyCredentialRequest: profileKeyCredentialRequestHex, // - a versioned profile using last known profileKeyVersion
userLanguages, // - 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) { 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) { if (profile.unrestrictedUnidentifiedAccess && profile.unidentifiedAccess) {
log.info( log.info(
`Setting sealedSender to UNRESTRICTED for conversation ${c.idForLogging()}` `getProfile: setting sealedSender to UNRESTRICTED for conversation ${idForLogging}`
); );
c.set({ c.set({
sealedSender: SEALED_SENDER.UNRESTRICTED, sealedSender: SEALED_SENDER.UNRESTRICTED,
@ -152,14 +201,14 @@ export async function getProfile(
if (haveCorrectKey) { if (haveCorrectKey) {
log.info( log.info(
`Setting sealedSender to ENABLED for conversation ${c.idForLogging()}` `getProfile: setting sealedSender to ENABLED for conversation ${idForLogging}`
); );
c.set({ c.set({
sealedSender: SEALED_SENDER.ENABLED, sealedSender: SEALED_SENDER.ENABLED,
}); });
} else { } else {
log.info( log.warn(
`Setting sealedSender to DISABLED for conversation ${c.idForLogging()}` `getProfile: setting sealedSender to DISABLED for conversation ${idForLogging}`
); );
c.set({ c.set({
sealedSender: SEALED_SENDER.DISABLED, sealedSender: SEALED_SENDER.DISABLED,
@ -167,20 +216,22 @@ export async function getProfile(
} }
} else { } else {
log.info( log.info(
`Setting sealedSender to DISABLED for conversation ${c.idForLogging()}` `getProfile: setting sealedSender to DISABLED for conversation ${idForLogging}`
); );
c.set({ c.set({
sealedSender: SEALED_SENDER.DISABLED, sealedSender: SEALED_SENDER.DISABLED,
}); });
} }
const rawDecryptionKey = c.get('profileKey') || lastProfile?.profileKey;
const decryptionKey = rawDecryptionKey
? Bytes.fromBase64(rawDecryptionKey)
: undefined;
if (profile.about) { if (profile.about) {
const key = c.get('profileKey'); if (decryptionKey) {
if (key) {
const keyBuffer = Bytes.fromBase64(key);
const decrypted = decryptProfile( const decrypted = decryptProfile(
Bytes.fromBase64(profile.about), Bytes.fromBase64(profile.about),
keyBuffer decryptionKey
); );
c.set('about', Bytes.toString(trimForDisplay(decrypted))); c.set('about', Bytes.toString(trimForDisplay(decrypted)));
} }
@ -189,12 +240,10 @@ export async function getProfile(
} }
if (profile.aboutEmoji) { if (profile.aboutEmoji) {
const key = c.get('profileKey'); if (decryptionKey) {
if (key) {
const keyBuffer = Bytes.fromBase64(key);
const decrypted = decryptProfile( const decrypted = decryptProfile(
Bytes.fromBase64(profile.aboutEmoji), Bytes.fromBase64(profile.aboutEmoji),
keyBuffer decryptionKey
); );
c.set('aboutEmoji', Bytes.toString(trimForDisplay(decrypted))); c.set('aboutEmoji', Bytes.toString(trimForDisplay(decrypted)));
} }
@ -243,7 +292,11 @@ export async function getProfile(
} }
} }
} catch (error) { } catch (error) {
switch (error?.code) { if (!(error instanceof HTTPError)) {
throw error;
}
switch (error.code) {
case 401: case 401:
case 403: case 403:
if ( if (
@ -251,46 +304,49 @@ export async function getProfile(
c.get('sealedSender') === SEALED_SENDER.UNRESTRICTED c.get('sealedSender') === SEALED_SENDER.UNRESTRICTED
) { ) {
log.warn( 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); c.setProfileKey(undefined);
} }
if (c.get('sealedSender') === SEALED_SENDER.UNKNOWN) { if (c.get('sealedSender') === SEALED_SENDER.UNKNOWN) {
log.warn( 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); c.set('sealedSender', SEALED_SENDER.DISABLED);
} }
return; return;
case 404:
log.info(
`getProfile: failed to find a profile for ${c.idForLogging()}`
);
c.setUnregistered();
return;
default: default:
log.warn( log.warn(
'getProfile failure:', 'getProfile failure:',
c.idForLogging(), idForLogging,
error && error.stack ? error.stack : error Errors.toLogFormat(error)
); );
return; return;
} }
} }
const decryptionKeyString = profileKey || lastProfile?.profileKey;
const decryptionKey = decryptionKeyString
? Bytes.fromBase64(decryptionKeyString)
: undefined;
let isSuccessfullyDecrypted = true;
if (profile.name) { if (profile.name) {
try { if (decryptionKey) {
await c.setEncryptedProfileName(profile.name); try {
} catch (error) { await c.setEncryptedProfileName(profile.name, decryptionKey);
log.warn( } catch (error) {
'getProfile decryption failure:', log.warn(
c.idForLogging(), 'getProfile decryption failure:',
error && error.stack ? error.stack : error idForLogging,
); Errors.toLogFormat(error)
await c.set({ );
profileName: undefined, isSuccessfullyDecrypted = false;
profileFamilyName: undefined, await c.set({
}); profileName: undefined,
profileFamilyName: undefined,
});
}
} }
} else { } else {
c.set({ c.set({
@ -300,17 +356,58 @@ export async function getProfile(
} }
try { try {
await c.setProfileAvatar(profile.avatar); if (decryptionKey) {
await c.setProfileAvatar(profile.avatar, decryptionKey);
}
} catch (error) { } catch (error) {
if (error.code === 403 || error.code === 404) { if (error instanceof HTTPError) {
log.info(`Clearing profile avatar for conversation ${c.idForLogging()}`); if (error.code === 403 || error.code === 404) {
c.set({ log.warn(
profileAvatar: null, `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()); 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); window.Signal.Data.updateConversation(c.attributes);
} }
export async function getProfile(
providedUuid?: string,
providedE164?: string
): Promise<void> {
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);
}