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
;
28 import java
.io
.IOException
;
29 import java
.io
.OutputStream
;
30 import java
.nio
.file
.Files
;
31 import java
.util
.Base64
;
32 import java
.util
.Date
;
33 import java
.util
.HashSet
;
34 import java
.util
.List
;
35 import java
.util
.Objects
;
38 import io
.reactivex
.rxjava3
.core
.Single
;
40 public final class ProfileHelper
{
42 private final static Logger logger
= LoggerFactory
.getLogger(ProfileHelper
.class);
44 private final SignalAccount account
;
45 private final SignalDependencies dependencies
;
46 private final AvatarStore avatarStore
;
47 private final UnidentifiedAccessProvider unidentifiedAccessProvider
;
48 private final SignalServiceAddressResolver addressResolver
;
51 final SignalAccount account
,
52 final SignalDependencies dependencies
,
53 final AvatarStore avatarStore
,
54 final UnidentifiedAccessProvider unidentifiedAccessProvider
,
55 final SignalServiceAddressResolver addressResolver
57 this.account
= account
;
58 this.dependencies
= dependencies
;
59 this.avatarStore
= avatarStore
;
60 this.unidentifiedAccessProvider
= unidentifiedAccessProvider
;
61 this.addressResolver
= addressResolver
;
64 public Profile
getRecipientProfile(RecipientId recipientId
) {
65 return getRecipientProfile(recipientId
, false);
68 public void refreshRecipientProfile(RecipientId recipientId
) {
69 getRecipientProfile(recipientId
, true);
72 public ProfileKeyCredential
getRecipientProfileKeyCredential(RecipientId recipientId
) {
73 var profileKeyCredential
= account
.getProfileStore().getProfileKeyCredential(recipientId
);
74 if (profileKeyCredential
!= null) {
75 return profileKeyCredential
;
78 ProfileAndCredential profileAndCredential
;
80 profileAndCredential
= retrieveProfileAndCredential(recipientId
,
81 SignalServiceProfile
.RequestType
.PROFILE_AND_CREDENTIAL
);
82 } catch (IOException e
) {
83 logger
.warn("Failed to retrieve profile key credential, ignoring: {}", e
.getMessage());
87 profileKeyCredential
= profileAndCredential
.getProfileKeyCredential().orNull();
88 account
.getProfileStore().storeProfileKeyCredential(recipientId
, profileKeyCredential
);
90 var profileKey
= account
.getProfileStore().getProfileKey(recipientId
);
91 if (profileKey
!= null) {
92 final var profile
= decryptProfileAndDownloadAvatar(recipientId
,
94 profileAndCredential
.getProfile());
95 account
.getProfileStore().storeProfile(recipientId
, profile
);
98 return profileKeyCredential
;
102 * @param givenName if null, the previous givenName will be kept
103 * @param familyName if null, the previous familyName will be kept
104 * @param about if null, the previous about text will be kept
105 * @param aboutEmoji if null, the previous about emoji will be kept
106 * @param avatar if avatar is null the image from the local avatar store is used (if present),
108 public void setProfile(
109 String givenName
, final String familyName
, String about
, String aboutEmoji
, Optional
<File
> avatar
110 ) throws IOException
{
111 setProfile(true, givenName
, familyName
, about
, aboutEmoji
, avatar
);
114 public void setProfile(
115 boolean uploadProfile
,
117 final String familyName
,
120 Optional
<File
> avatar
121 ) throws IOException
{
122 var profile
= getRecipientProfile(account
.getSelfRecipientId());
123 var builder
= profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
);
124 if (givenName
!= null) {
125 builder
.withGivenName(givenName
);
127 if (familyName
!= null) {
128 builder
.withFamilyName(familyName
);
131 builder
.withAbout(about
);
133 if (aboutEmoji
!= null) {
134 builder
.withAboutEmoji(aboutEmoji
);
136 var newProfile
= builder
.build();
139 try (final var streamDetails
= avatar
== null
140 ? avatarStore
.retrieveProfileAvatar(account
.getSelfAddress())
141 : avatar
.isPresent() ? Utils
.createStreamDetailsFromFile(avatar
.get()) : null) {
142 final var avatarPath
= dependencies
.getAccountManager()
143 .setVersionedProfile(account
.getAci(),
144 account
.getProfileKey(),
145 newProfile
.getInternalServiceName(),
146 newProfile
.getAbout() == null ?
"" : newProfile
.getAbout(),
147 newProfile
.getAboutEmoji() == null ?
"" : newProfile
.getAboutEmoji(),
150 List
.of(/* TODO */));
151 builder
.withAvatarUrlPath(avatarPath
.orNull());
152 newProfile
= builder
.build();
156 if (avatar
!= null) {
157 if (avatar
.isPresent()) {
158 avatarStore
.storeProfileAvatar(account
.getSelfAddress(),
159 outputStream
-> IOUtils
.copyFileToStream(avatar
.get(), outputStream
));
161 avatarStore
.deleteProfileAvatar(account
.getSelfAddress());
164 account
.getProfileStore().storeProfile(account
.getSelfRecipientId(), newProfile
);
167 private final Set
<RecipientId
> pendingProfileRequest
= new HashSet
<>();
169 private Profile
getRecipientProfile(RecipientId recipientId
, boolean force
) {
170 var profile
= account
.getProfileStore().getProfile(recipientId
);
172 var now
= System
.currentTimeMillis();
173 // Profiles are cached for 24h before retrieving them again, unless forced
174 if (!force
&& profile
!= null && now
- profile
.getLastUpdateTimestamp() < 6 * 60 * 60 * 1000) {
178 synchronized (pendingProfileRequest
) {
179 if (pendingProfileRequest
.contains(recipientId
)) {
182 pendingProfileRequest
.add(recipientId
);
184 final SignalServiceProfile encryptedProfile
;
186 encryptedProfile
= retrieveEncryptedProfile(recipientId
);
188 synchronized (pendingProfileRequest
) {
189 pendingProfileRequest
.remove(recipientId
);
193 Profile newProfile
= null;
194 if (encryptedProfile
!= null) {
195 var profileKey
= account
.getProfileStore().getProfileKey(recipientId
);
196 if (profileKey
!= null) {
197 newProfile
= decryptProfileAndDownloadAvatar(recipientId
, profileKey
, encryptedProfile
);
198 if (newProfile
== null) {
199 account
.getProfileStore().storeProfileKey(recipientId
, null);
203 if (newProfile
== null) {
205 profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
)
206 ).withLastUpdateTimestamp(System
.currentTimeMillis())
207 .withUnidentifiedAccessMode(ProfileUtils
.getUnidentifiedAccessMode(encryptedProfile
, null))
208 .withCapabilities(ProfileUtils
.getCapabilities(encryptedProfile
))
213 if (newProfile
== null) {
215 profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
)
216 ).withLastUpdateTimestamp(now
)
217 .withUnidentifiedAccessMode(Profile
.UnidentifiedAccessMode
.UNKNOWN
)
218 .withCapabilities(Set
.of())
222 account
.getProfileStore().storeProfile(recipientId
, newProfile
);
227 private SignalServiceProfile
retrieveEncryptedProfile(RecipientId recipientId
) {
229 return retrieveProfileAndCredential(recipientId
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
230 } catch (IOException e
) {
231 logger
.warn("Failed to retrieve profile, ignoring: {}", e
.getMessage());
236 private SignalServiceProfile
retrieveProfileSync(String username
) throws IOException
{
237 final var locale
= Utils
.getDefaultLocale();
238 return dependencies
.getMessageReceiver().retrieveProfileByUsername(username
, Optional
.absent(), locale
);
241 private ProfileAndCredential
retrieveProfileAndCredential(
242 final RecipientId recipientId
, final SignalServiceProfile
.RequestType requestType
243 ) throws IOException
{
244 final var profileAndCredential
= retrieveProfileSync(recipientId
, requestType
);
245 final var profile
= profileAndCredential
.getProfile();
248 var newIdentity
= account
.getIdentityKeyStore()
249 .saveIdentity(recipientId
,
250 new IdentityKey(Base64
.getDecoder().decode(profile
.getIdentityKey())),
254 account
.getSessionStore().archiveSessions(recipientId
);
255 account
.getSenderKeyStore().deleteSharedWith(recipientId
);
257 } catch (InvalidKeyException ignored
) {
258 logger
.warn("Got invalid identity key in profile for {}",
259 addressResolver
.resolveSignalServiceAddress(recipientId
).getIdentifier());
261 return profileAndCredential
;
264 private Profile
decryptProfileAndDownloadAvatar(
265 final RecipientId recipientId
, final ProfileKey profileKey
, final SignalServiceProfile encryptedProfile
267 final var avatarPath
= encryptedProfile
.getAvatar();
268 downloadProfileAvatar(recipientId
, avatarPath
, profileKey
);
270 return ProfileUtils
.decryptProfile(profileKey
, encryptedProfile
);
273 public void downloadProfileAvatar(
274 final RecipientId recipientId
, final String avatarPath
, final ProfileKey profileKey
276 var profile
= account
.getProfileStore().getProfile(recipientId
);
277 if (profile
== null || !Objects
.equals(avatarPath
, profile
.getAvatarUrlPath())) {
278 downloadProfileAvatar(addressResolver
.resolveSignalServiceAddress(recipientId
), avatarPath
, profileKey
);
279 var builder
= profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
);
280 account
.getProfileStore().storeProfile(recipientId
, builder
.withAvatarUrlPath(avatarPath
).build());
284 private ProfileAndCredential
retrieveProfileSync(
285 RecipientId recipientId
, SignalServiceProfile
.RequestType requestType
286 ) throws IOException
{
288 return retrieveProfile(recipientId
, requestType
).blockingGet();
289 } catch (RuntimeException e
) {
290 if (e
.getCause() instanceof PushNetworkException
) {
291 throw (PushNetworkException
) e
.getCause();
292 } else if (e
.getCause() instanceof NotFoundException
) {
293 throw (NotFoundException
) e
.getCause();
295 throw new IOException(e
);
300 private Single
<ProfileAndCredential
> retrieveProfile(
301 RecipientId recipientId
, SignalServiceProfile
.RequestType requestType
303 var unidentifiedAccess
= getUnidentifiedAccess(recipientId
);
304 var profileKey
= Optional
.fromNullable(account
.getProfileStore().getProfileKey(recipientId
));
306 final var address
= addressResolver
.resolveSignalServiceAddress(recipientId
);
307 return retrieveProfile(address
, profileKey
, unidentifiedAccess
, requestType
);
310 private Single
<ProfileAndCredential
> retrieveProfile(
311 SignalServiceAddress address
,
312 Optional
<ProfileKey
> profileKey
,
313 Optional
<UnidentifiedAccess
> unidentifiedAccess
,
314 SignalServiceProfile
.RequestType requestType
316 final var profileService
= dependencies
.getProfileService();
317 final var locale
= Utils
.getDefaultLocale();
319 return profileService
.getProfile(address
, profileKey
, unidentifiedAccess
, requestType
, locale
).map(pair
-> {
320 var processor
= new ProfileService
.ProfileResponseProcessor(pair
);
321 if (processor
.hasResult()) {
322 return processor
.getResult();
323 } else if (processor
.notFound()) {
324 throw new NotFoundException("Profile not found");
326 throw pair
.getExecutionError()
327 .or(pair
.getApplicationError())
328 .or(new IOException("Unknown error while retrieving profile"));
333 private void downloadProfileAvatar(
334 SignalServiceAddress address
, String avatarPath
, ProfileKey profileKey
336 if (avatarPath
== null) {
338 avatarStore
.deleteProfileAvatar(address
);
339 } catch (IOException e
) {
340 logger
.warn("Failed to delete local profile avatar, ignoring: {}", e
.getMessage());
346 avatarStore
.storeProfileAvatar(address
,
347 outputStream
-> retrieveProfileAvatar(avatarPath
, profileKey
, outputStream
));
348 } catch (Throwable e
) {
349 if (e
instanceof AssertionError
&& e
.getCause() instanceof InterruptedException
) {
350 Thread
.currentThread().interrupt();
352 logger
.warn("Failed to download profile avatar, ignoring: {}", e
.getMessage());
356 private void retrieveProfileAvatar(
357 String avatarPath
, ProfileKey profileKey
, OutputStream outputStream
358 ) throws IOException
{
359 var tmpFile
= IOUtils
.createTempFile();
360 try (var input
= dependencies
.getMessageReceiver()
361 .retrieveProfileAvatar(avatarPath
,
364 ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
)) {
365 // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
366 IOUtils
.copyStream(input
, outputStream
, (int) ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
);
369 Files
.delete(tmpFile
.toPath());
370 } catch (IOException e
) {
371 logger
.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}",
378 private Optional
<UnidentifiedAccess
> getUnidentifiedAccess(RecipientId recipientId
) {
379 var unidentifiedAccess
= unidentifiedAccessProvider
.getAccessFor(recipientId
);
381 if (unidentifiedAccess
.isPresent()) {
382 return unidentifiedAccess
.get().getTargetUnidentifiedAccess();
385 return Optional
.absent();