2021-07-21 20:45:41 +00:00
|
|
|
// Copyright 2020-2021 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2021-11-11 15:24:28 +00:00
|
|
|
import type { ProfileKeyCredentialRequestContext } from '@signalapp/signal-client/zkgroup';
|
2021-07-21 20:45:41 +00:00
|
|
|
import { SEALED_SENDER } from '../types/SealedSender';
|
2021-09-10 02:38:11 +00:00
|
|
|
import { Address } from '../types/Address';
|
|
|
|
import { QualifiedAddress } from '../types/QualifiedAddress';
|
2021-09-24 00:49:05 +00:00
|
|
|
import * as Bytes from '../Bytes';
|
|
|
|
import { trimForDisplay, verifyAccessKey, decryptProfile } from '../Crypto';
|
2021-07-21 20:45:41 +00:00
|
|
|
import {
|
|
|
|
generateProfileKeyCredentialRequest,
|
|
|
|
getClientZkProfileOperations,
|
|
|
|
handleProfileKeyCredential,
|
|
|
|
} from './zkgroup';
|
|
|
|
import { getSendOptions } from './getSendOptions';
|
|
|
|
import { isMe } from './whatTypeOfConversation';
|
2021-09-17 18:27:53 +00:00
|
|
|
import * as log from '../logging/log';
|
2021-11-02 23:01:13 +00:00
|
|
|
import { getUserLanguages } from './userLanguages';
|
|
|
|
import { parseBadgesFromServer } from '../badges/parseBadgesFromServer';
|
2021-07-21 20:45:41 +00:00
|
|
|
|
|
|
|
export async function getProfile(
|
|
|
|
providedUuid?: string,
|
|
|
|
providedE164?: string
|
|
|
|
): Promise<void> {
|
|
|
|
if (!window.textsecure.messaging) {
|
|
|
|
throw new Error(
|
|
|
|
'Conversation.getProfile: window.textsecure.messaging not available'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-11-02 23:01:13 +00:00
|
|
|
const { updatesUrl } = window.SignalContext.config;
|
|
|
|
if (typeof updatesUrl !== 'string') {
|
|
|
|
throw new Error('getProfile expected updatesUrl to be a defined string');
|
|
|
|
}
|
|
|
|
|
2021-07-21 20:45:41 +00:00
|
|
|
const id = window.ConversationController.ensureContactIds({
|
|
|
|
uuid: providedUuid,
|
|
|
|
e164: providedE164,
|
|
|
|
});
|
|
|
|
const c = window.ConversationController.get(id);
|
|
|
|
if (!c) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.error('getProfile: failed to find conversation; doing nothing');
|
2021-07-21 20:45:41 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const clientZkProfileCipher = getClientZkProfileOperations(
|
|
|
|
window.getServerPublicParams()
|
|
|
|
);
|
|
|
|
|
2021-11-02 23:01:13 +00:00
|
|
|
const userLanguages = getUserLanguages(
|
|
|
|
navigator.languages,
|
|
|
|
window.getLocale()
|
|
|
|
);
|
|
|
|
|
2021-07-21 20:45:41 +00:00
|
|
|
let profile;
|
|
|
|
|
|
|
|
try {
|
|
|
|
await Promise.all([
|
|
|
|
c.deriveAccessKeyIfNeeded(),
|
|
|
|
c.deriveProfileKeyVersionIfNeeded(),
|
|
|
|
]);
|
|
|
|
|
|
|
|
const profileKey = c.get('profileKey');
|
2021-12-03 02:06:32 +00:00
|
|
|
const uuid = c.getCheckedUuid('getProfile');
|
|
|
|
const profileKeyVersionHex = c.get('profileKeyVersion');
|
|
|
|
if (!profileKeyVersionHex) {
|
|
|
|
throw new Error('No profile key version available');
|
|
|
|
}
|
2021-07-21 20:45:41 +00:00
|
|
|
const existingProfileKeyCredential = c.get('profileKeyCredential');
|
|
|
|
|
|
|
|
let profileKeyCredentialRequestHex: undefined | string;
|
|
|
|
let profileCredentialRequestContext:
|
|
|
|
| undefined
|
|
|
|
| ProfileKeyCredentialRequestContext;
|
|
|
|
|
2021-12-03 02:06:32 +00:00
|
|
|
if (profileKey && profileKeyVersionHex && !existingProfileKeyCredential) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info('Generating request...');
|
2021-07-21 20:45:41 +00:00
|
|
|
({
|
|
|
|
requestHex: profileKeyCredentialRequestHex,
|
|
|
|
context: profileCredentialRequestContext,
|
|
|
|
} = generateProfileKeyCredentialRequest(
|
|
|
|
clientZkProfileCipher,
|
2021-12-03 02:06:32 +00:00
|
|
|
uuid.toString(),
|
2021-07-21 20:45:41 +00:00
|
|
|
profileKey
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
const { sendMetadata = {} } = await getSendOptions(c.attributes);
|
2021-12-03 02:06:32 +00:00
|
|
|
const getInfo = sendMetadata[uuid.toString()] || {};
|
2021-07-21 20:45:41 +00:00
|
|
|
|
|
|
|
if (getInfo.accessKey) {
|
|
|
|
try {
|
2021-12-03 02:06:32 +00:00
|
|
|
profile = await window.textsecure.messaging.getProfile(uuid, {
|
2021-07-21 20:45:41 +00:00
|
|
|
accessKey: getInfo.accessKey,
|
|
|
|
profileKeyVersion: profileKeyVersionHex,
|
|
|
|
profileKeyCredentialRequest: profileKeyCredentialRequestHex,
|
2021-11-02 23:01:13 +00:00
|
|
|
userLanguages,
|
2021-07-21 20:45:41 +00:00
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
if (error.code === 401 || error.code === 403) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info(
|
2021-07-21 20:45:41 +00:00
|
|
|
`Setting sealedSender to DISABLED for conversation ${c.idForLogging()}`
|
|
|
|
);
|
|
|
|
c.set({ sealedSender: SEALED_SENDER.DISABLED });
|
2021-12-03 02:06:32 +00:00
|
|
|
profile = await window.textsecure.messaging.getProfile(uuid, {
|
2021-07-21 20:45:41 +00:00
|
|
|
profileKeyVersion: profileKeyVersionHex,
|
|
|
|
profileKeyCredentialRequest: profileKeyCredentialRequestHex,
|
2021-11-02 23:01:13 +00:00
|
|
|
userLanguages,
|
2021-07-21 20:45:41 +00:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2021-12-03 02:06:32 +00:00
|
|
|
profile = await window.textsecure.messaging.getProfile(uuid, {
|
2021-07-21 20:45:41 +00:00
|
|
|
profileKeyVersion: profileKeyVersionHex,
|
|
|
|
profileKeyCredentialRequest: profileKeyCredentialRequestHex,
|
2021-11-02 23:01:13 +00:00
|
|
|
userLanguages,
|
2021-07-21 20:45:41 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-10-05 22:10:08 +00:00
|
|
|
if (profile.identityKey) {
|
|
|
|
const identityKey = Bytes.fromBase64(profile.identityKey);
|
|
|
|
const changed = await window.textsecure.storage.protocol.saveIdentity(
|
2021-12-03 02:06:32 +00:00
|
|
|
new Address(uuid, 1),
|
2021-10-05 22:10:08 +00:00
|
|
|
identityKey,
|
|
|
|
false
|
2021-07-21 20:45:41 +00:00
|
|
|
);
|
2021-10-05 22:10:08 +00:00
|
|
|
if (changed) {
|
|
|
|
// save identity will close all sessions except for .1, so we
|
|
|
|
// must close that one manually.
|
|
|
|
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
|
|
|
|
await window.textsecure.storage.protocol.archiveSession(
|
2021-12-03 02:06:32 +00:00
|
|
|
new QualifiedAddress(ourUuid, new Address(uuid, 1))
|
2021-10-05 22:10:08 +00:00
|
|
|
);
|
|
|
|
}
|
2021-07-21 20:45:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const accessKey = c.get('accessKey');
|
|
|
|
if (profile.unrestrictedUnidentifiedAccess && profile.unidentifiedAccess) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info(
|
2021-07-21 20:45:41 +00:00
|
|
|
`Setting sealedSender to UNRESTRICTED for conversation ${c.idForLogging()}`
|
|
|
|
);
|
|
|
|
c.set({
|
|
|
|
sealedSender: SEALED_SENDER.UNRESTRICTED,
|
|
|
|
});
|
|
|
|
} else if (accessKey && profile.unidentifiedAccess) {
|
2021-09-24 00:49:05 +00:00
|
|
|
const haveCorrectKey = verifyAccessKey(
|
|
|
|
Bytes.fromBase64(accessKey),
|
|
|
|
Bytes.fromBase64(profile.unidentifiedAccess)
|
2021-07-21 20:45:41 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
if (haveCorrectKey) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info(
|
2021-07-21 20:45:41 +00:00
|
|
|
`Setting sealedSender to ENABLED for conversation ${c.idForLogging()}`
|
|
|
|
);
|
|
|
|
c.set({
|
|
|
|
sealedSender: SEALED_SENDER.ENABLED,
|
|
|
|
});
|
|
|
|
} else {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info(
|
2021-07-21 20:45:41 +00:00
|
|
|
`Setting sealedSender to DISABLED for conversation ${c.idForLogging()}`
|
|
|
|
);
|
|
|
|
c.set({
|
|
|
|
sealedSender: SEALED_SENDER.DISABLED,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info(
|
2021-07-21 20:45:41 +00:00
|
|
|
`Setting sealedSender to DISABLED for conversation ${c.idForLogging()}`
|
|
|
|
);
|
|
|
|
c.set({
|
|
|
|
sealedSender: SEALED_SENDER.DISABLED,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (profile.about) {
|
|
|
|
const key = c.get('profileKey');
|
|
|
|
if (key) {
|
2021-09-24 00:49:05 +00:00
|
|
|
const keyBuffer = Bytes.fromBase64(key);
|
|
|
|
const decrypted = decryptProfile(
|
|
|
|
Bytes.fromBase64(profile.about),
|
2021-07-21 20:45:41 +00:00
|
|
|
keyBuffer
|
|
|
|
);
|
2021-09-24 00:49:05 +00:00
|
|
|
c.set('about', Bytes.toString(trimForDisplay(decrypted)));
|
2021-07-21 20:45:41 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
c.unset('about');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (profile.aboutEmoji) {
|
|
|
|
const key = c.get('profileKey');
|
|
|
|
if (key) {
|
2021-09-24 00:49:05 +00:00
|
|
|
const keyBuffer = Bytes.fromBase64(key);
|
|
|
|
const decrypted = decryptProfile(
|
|
|
|
Bytes.fromBase64(profile.aboutEmoji),
|
2021-07-21 20:45:41 +00:00
|
|
|
keyBuffer
|
|
|
|
);
|
2021-09-24 00:49:05 +00:00
|
|
|
c.set('aboutEmoji', Bytes.toString(trimForDisplay(decrypted)));
|
2021-07-21 20:45:41 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
c.unset('aboutEmoji');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (profile.paymentAddress && isMe(c.attributes)) {
|
|
|
|
window.storage.put('paymentAddress', profile.paymentAddress);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (profile.capabilities) {
|
|
|
|
c.set({ capabilities: profile.capabilities });
|
|
|
|
} else {
|
|
|
|
c.unset('capabilities');
|
|
|
|
}
|
|
|
|
|
2021-11-02 23:01:13 +00:00
|
|
|
const badges = parseBadgesFromServer(profile.badges, updatesUrl);
|
|
|
|
if (badges.length) {
|
|
|
|
await window.reduxActions.badges.updateOrCreate(badges);
|
|
|
|
c.set({
|
|
|
|
badges: badges.map(badge => ({
|
|
|
|
id: badge.id,
|
|
|
|
...('expiresAt' in badge
|
|
|
|
? {
|
|
|
|
expiresAt: badge.expiresAt,
|
|
|
|
isVisible: badge.isVisible,
|
|
|
|
}
|
|
|
|
: {}),
|
|
|
|
})),
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
c.unset('badges');
|
|
|
|
}
|
|
|
|
|
2021-07-21 20:45:41 +00:00
|
|
|
if (profileCredentialRequestContext) {
|
|
|
|
if (profile.credential) {
|
|
|
|
const profileKeyCredential = handleProfileKeyCredential(
|
|
|
|
clientZkProfileCipher,
|
|
|
|
profileCredentialRequestContext,
|
|
|
|
profile.credential
|
|
|
|
);
|
|
|
|
c.set({ profileKeyCredential });
|
|
|
|
} else {
|
|
|
|
c.unset('profileKeyCredential');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
switch (error?.code) {
|
|
|
|
case 403:
|
|
|
|
throw error;
|
|
|
|
case 404:
|
2021-09-17 18:27:53 +00:00
|
|
|
log.warn(
|
2021-07-21 20:45:41 +00:00
|
|
|
`getProfile failure: failed to find a profile for ${c.idForLogging()}`,
|
|
|
|
error && error.stack ? error.stack : error
|
|
|
|
);
|
|
|
|
c.setUnregistered();
|
|
|
|
return;
|
|
|
|
default:
|
2021-09-17 18:27:53 +00:00
|
|
|
log.warn(
|
2021-07-21 20:45:41 +00:00
|
|
|
'getProfile failure:',
|
|
|
|
c.idForLogging(),
|
|
|
|
error && error.stack ? error.stack : error
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-05 22:10:08 +00:00
|
|
|
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,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
c.set({
|
2021-07-21 20:45:41 +00:00
|
|
|
profileName: undefined,
|
|
|
|
profileFamilyName: undefined,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
await c.setProfileAvatar(profile.avatar);
|
|
|
|
} catch (error) {
|
|
|
|
if (error.code === 403 || error.code === 404) {
|
2021-09-17 18:27:53 +00:00
|
|
|
log.info(`Clearing profile avatar for conversation ${c.idForLogging()}`);
|
2021-07-21 20:45:41 +00:00
|
|
|
c.set({
|
|
|
|
profileAvatar: null,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
c.set('profileLastFetchedAt', Date.now());
|
|
|
|
|
|
|
|
window.Signal.Data.updateConversation(c.attributes);
|
|
|
|
}
|