1 package org
.asamk
.signal
.manager
.helper
;
3 import org
.asamk
.signal
.manager
.AvatarStore
;
4 import org
.asamk
.signal
.manager
.SignalDependencies
;
5 import org
.asamk
.signal
.manager
.config
.ServiceConfig
;
6 import org
.asamk
.signal
.manager
.storage
.SignalAccount
;
7 import org
.asamk
.signal
.manager
.storage
.recipients
.Profile
;
8 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientId
;
9 import org
.asamk
.signal
.manager
.util
.IOUtils
;
10 import org
.asamk
.signal
.manager
.util
.ProfileUtils
;
11 import org
.asamk
.signal
.manager
.util
.Utils
;
12 import org
.signal
.zkgroup
.profiles
.ProfileKey
;
13 import org
.signal
.zkgroup
.profiles
.ProfileKeyCredential
;
14 import org
.slf4j
.Logger
;
15 import org
.slf4j
.LoggerFactory
;
16 import org
.whispersystems
.libsignal
.IdentityKey
;
17 import org
.whispersystems
.libsignal
.InvalidKeyException
;
18 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
19 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccess
;
20 import org
.whispersystems
.signalservice
.api
.profiles
.ProfileAndCredential
;
21 import org
.whispersystems
.signalservice
.api
.profiles
.SignalServiceProfile
;
22 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
23 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NotFoundException
;
24 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.PushNetworkException
;
25 import org
.whispersystems
.signalservice
.api
.services
.ProfileService
;
26 import org
.whispersystems
.signalservice
.internal
.ServiceResponse
;
29 import java
.io
.IOException
;
30 import java
.io
.OutputStream
;
31 import java
.nio
.file
.Files
;
32 import java
.util
.Base64
;
33 import java
.util
.Date
;
34 import java
.util
.HashSet
;
37 import io
.reactivex
.rxjava3
.core
.Single
;
39 public final class ProfileHelper
{
41 private final static Logger logger
= LoggerFactory
.getLogger(ProfileHelper
.class);
43 private final SignalAccount account
;
44 private final SignalDependencies dependencies
;
45 private final AvatarStore avatarStore
;
46 private final ProfileKeyProvider profileKeyProvider
;
47 private final UnidentifiedAccessProvider unidentifiedAccessProvider
;
48 private final ProfileServiceProvider profileServiceProvider
;
49 private final MessageReceiverProvider messageReceiverProvider
;
50 private final SignalServiceAddressResolver addressResolver
;
53 final SignalAccount account
,
54 final SignalDependencies dependencies
,
55 final AvatarStore avatarStore
,
56 final ProfileKeyProvider profileKeyProvider
,
57 final UnidentifiedAccessProvider unidentifiedAccessProvider
,
58 final ProfileServiceProvider profileServiceProvider
,
59 final MessageReceiverProvider messageReceiverProvider
,
60 final SignalServiceAddressResolver addressResolver
62 this.account
= account
;
63 this.dependencies
= dependencies
;
64 this.avatarStore
= avatarStore
;
65 this.profileKeyProvider
= profileKeyProvider
;
66 this.unidentifiedAccessProvider
= unidentifiedAccessProvider
;
67 this.profileServiceProvider
= profileServiceProvider
;
68 this.messageReceiverProvider
= messageReceiverProvider
;
69 this.addressResolver
= addressResolver
;
72 public Profile
getRecipientProfile(RecipientId recipientId
) {
73 return getRecipientProfile(recipientId
, false);
76 public void refreshRecipientProfile(RecipientId recipientId
) {
77 getRecipientProfile(recipientId
, true);
80 public ProfileKeyCredential
getRecipientProfileKeyCredential(RecipientId recipientId
) {
81 var profileKeyCredential
= account
.getProfileStore().getProfileKeyCredential(recipientId
);
82 if (profileKeyCredential
!= null) {
83 return profileKeyCredential
;
86 ProfileAndCredential profileAndCredential
;
88 profileAndCredential
= retrieveProfileAndCredential(recipientId
,
89 SignalServiceProfile
.RequestType
.PROFILE_AND_CREDENTIAL
);
90 } catch (IOException e
) {
91 logger
.warn("Failed to retrieve profile key credential, ignoring: {}", e
.getMessage());
95 profileKeyCredential
= profileAndCredential
.getProfileKeyCredential().orNull();
96 account
.getProfileStore().storeProfileKeyCredential(recipientId
, profileKeyCredential
);
98 var profileKey
= account
.getProfileStore().getProfileKey(recipientId
);
99 if (profileKey
!= null) {
100 final var profile
= decryptProfileAndDownloadAvatar(recipientId
,
102 profileAndCredential
.getProfile());
103 account
.getProfileStore().storeProfile(recipientId
, profile
);
106 return profileKeyCredential
;
110 * @param givenName if null, the previous givenName will be kept
111 * @param familyName if null, the previous familyName will be kept
112 * @param about if null, the previous about text will be kept
113 * @param aboutEmoji if null, the previous about emoji will be kept
114 * @param avatar if avatar is null the image from the local avatar store is used (if present),
116 public void setProfile(
117 String givenName
, final String familyName
, String about
, String aboutEmoji
, Optional
<File
> avatar
118 ) throws IOException
{
119 var profile
= getRecipientProfile(account
.getSelfRecipientId());
120 var builder
= profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
);
121 if (givenName
!= null) {
122 builder
.withGivenName(givenName
);
124 if (familyName
!= null) {
125 builder
.withFamilyName(familyName
);
128 builder
.withAbout(about
);
130 if (aboutEmoji
!= null) {
131 builder
.withAboutEmoji(aboutEmoji
);
133 var newProfile
= builder
.build();
135 try (final var streamDetails
= avatar
== null
136 ? avatarStore
.retrieveProfileAvatar(account
.getSelfAddress())
137 : avatar
.isPresent() ? Utils
.createStreamDetailsFromFile(avatar
.get()) : null) {
138 dependencies
.getAccountManager()
139 .setVersionedProfile(account
.getUuid(),
140 account
.getProfileKey(),
141 newProfile
.getInternalServiceName(),
142 newProfile
.getAbout() == null ?
"" : newProfile
.getAbout(),
143 newProfile
.getAboutEmoji() == null ?
"" : newProfile
.getAboutEmoji(),
148 if (avatar
!= null) {
149 if (avatar
.isPresent()) {
150 avatarStore
.storeProfileAvatar(account
.getSelfAddress(),
151 outputStream
-> IOUtils
.copyFileToStream(avatar
.get(), outputStream
));
153 avatarStore
.deleteProfileAvatar(account
.getSelfAddress());
156 account
.getProfileStore().storeProfile(account
.getSelfRecipientId(), newProfile
);
159 private final Set
<RecipientId
> pendingProfileRequest
= new HashSet
<>();
161 private Profile
getRecipientProfile(RecipientId recipientId
, boolean force
) {
162 var profile
= account
.getProfileStore().getProfile(recipientId
);
164 var now
= System
.currentTimeMillis();
165 // Profiles are cached for 24h before retrieving them again, unless forced
166 if (!force
&& profile
!= null && now
- profile
.getLastUpdateTimestamp() < 24 * 60 * 60 * 1000) {
170 synchronized (pendingProfileRequest
) {
171 if (pendingProfileRequest
.contains(recipientId
)) {
174 pendingProfileRequest
.add(recipientId
);
176 final SignalServiceProfile encryptedProfile
;
178 encryptedProfile
= retrieveEncryptedProfile(recipientId
);
180 synchronized (pendingProfileRequest
) {
181 pendingProfileRequest
.remove(recipientId
);
184 if (encryptedProfile
== null) {
188 profile
= decryptProfileIfKeyKnown(recipientId
, encryptedProfile
);
189 account
.getProfileStore().storeProfile(recipientId
, profile
);
194 private Profile
decryptProfileIfKeyKnown(
195 final RecipientId recipientId
, final SignalServiceProfile encryptedProfile
197 var profileKey
= account
.getProfileStore().getProfileKey(recipientId
);
198 if (profileKey
== null) {
199 return new Profile(System
.currentTimeMillis(),
204 ProfileUtils
.getUnidentifiedAccessMode(encryptedProfile
, null),
205 ProfileUtils
.getCapabilities(encryptedProfile
));
208 return decryptProfileAndDownloadAvatar(recipientId
, profileKey
, encryptedProfile
);
211 private SignalServiceProfile
retrieveEncryptedProfile(RecipientId recipientId
) {
213 return retrieveProfileAndCredential(recipientId
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
214 } catch (IOException e
) {
215 logger
.warn("Failed to retrieve profile, ignoring: {}", e
.getMessage());
220 private SignalServiceProfile
retrieveProfileSync(String username
) throws IOException
{
221 return messageReceiverProvider
.getMessageReceiver().retrieveProfileByUsername(username
, Optional
.absent());
224 private ProfileAndCredential
retrieveProfileAndCredential(
225 final RecipientId recipientId
, final SignalServiceProfile
.RequestType requestType
226 ) throws IOException
{
227 final var profileAndCredential
= retrieveProfileSync(recipientId
, requestType
);
228 final var profile
= profileAndCredential
.getProfile();
231 var newIdentity
= account
.getIdentityKeyStore()
232 .saveIdentity(recipientId
,
233 new IdentityKey(Base64
.getDecoder().decode(profile
.getIdentityKey())),
237 account
.getSessionStore().archiveSessions(recipientId
);
239 } catch (InvalidKeyException ignored
) {
240 logger
.warn("Got invalid identity key in profile for {}",
241 addressResolver
.resolveSignalServiceAddress(recipientId
).getIdentifier());
243 return profileAndCredential
;
246 private Profile
decryptProfileAndDownloadAvatar(
247 final RecipientId recipientId
, final ProfileKey profileKey
, final SignalServiceProfile encryptedProfile
249 if (encryptedProfile
.getAvatar() != null) {
250 downloadProfileAvatar(addressResolver
.resolveSignalServiceAddress(recipientId
),
251 encryptedProfile
.getAvatar(),
255 return ProfileUtils
.decryptProfile(profileKey
, encryptedProfile
);
258 private ProfileAndCredential
retrieveProfileSync(
259 RecipientId recipientId
, SignalServiceProfile
.RequestType requestType
260 ) throws IOException
{
262 return retrieveProfile(recipientId
, requestType
).blockingGet();
263 } catch (RuntimeException e
) {
264 if (e
.getCause() instanceof PushNetworkException
) {
265 throw (PushNetworkException
) e
.getCause();
266 } else if (e
.getCause() instanceof NotFoundException
) {
267 throw (NotFoundException
) e
.getCause();
269 throw new IOException(e
);
274 private Single
<ProfileAndCredential
> retrieveProfile(
275 RecipientId recipientId
, SignalServiceProfile
.RequestType requestType
277 var unidentifiedAccess
= getUnidentifiedAccess(recipientId
);
278 var profileKey
= Optional
.fromNullable(profileKeyProvider
.getProfileKey(recipientId
));
280 final var address
= addressResolver
.resolveSignalServiceAddress(recipientId
);
281 return retrieveProfile(address
, profileKey
, unidentifiedAccess
, requestType
);
284 private Single
<ProfileAndCredential
> retrieveProfile(
285 SignalServiceAddress address
,
286 Optional
<ProfileKey
> profileKey
,
287 Optional
<UnidentifiedAccess
> unidentifiedAccess
,
288 SignalServiceProfile
.RequestType requestType
290 var profileService
= profileServiceProvider
.getProfileService();
292 Single
<ServiceResponse
<ProfileAndCredential
>> responseSingle
;
294 responseSingle
= profileService
.getProfile(address
, profileKey
, unidentifiedAccess
, requestType
);
295 } catch (NoClassDefFoundError e
) {
296 // Native zkgroup lib not available for ProfileKey
297 responseSingle
= profileService
.getProfile(address
, Optional
.absent(), unidentifiedAccess
, requestType
);
300 return responseSingle
.map(pair
-> {
301 var processor
= new ProfileService
.ProfileResponseProcessor(pair
);
302 if (processor
.hasResult()) {
303 return processor
.getResult();
304 } else if (processor
.notFound()) {
305 throw new NotFoundException("Profile not found");
307 throw pair
.getExecutionError()
308 .or(pair
.getApplicationError())
309 .or(new IOException("Unknown error while retrieving profile"));
314 private void downloadProfileAvatar(
315 SignalServiceAddress address
, String avatarPath
, ProfileKey profileKey
318 avatarStore
.storeProfileAvatar(address
,
319 outputStream
-> retrieveProfileAvatar(avatarPath
, profileKey
, outputStream
));
320 } catch (Throwable e
) {
321 if (e
instanceof AssertionError
&& e
.getCause() instanceof InterruptedException
) {
322 Thread
.currentThread().interrupt();
324 logger
.warn("Failed to download profile avatar, ignoring: {}", e
.getMessage());
328 private void retrieveProfileAvatar(
329 String avatarPath
, ProfileKey profileKey
, OutputStream outputStream
330 ) throws IOException
{
331 var tmpFile
= IOUtils
.createTempFile();
332 try (var input
= dependencies
.getMessageReceiver()
333 .retrieveProfileAvatar(avatarPath
,
336 ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
)) {
337 // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
338 IOUtils
.copyStream(input
, outputStream
, (int) ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
);
341 Files
.delete(tmpFile
.toPath());
342 } catch (IOException e
) {
343 logger
.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}",
350 private Optional
<UnidentifiedAccess
> getUnidentifiedAccess(RecipientId recipientId
) {
351 var unidentifiedAccess
= unidentifiedAccessProvider
.getAccessFor(recipientId
);
353 if (unidentifiedAccess
.isPresent()) {
354 return unidentifiedAccess
.get().getTargetUnidentifiedAccess();
357 return Optional
.absent();