1 package org
.asamk
.signal
.manager
.helper
;
3 import org
.asamk
.signal
.manager
.SignalDependencies
;
4 import org
.asamk
.signal
.manager
.config
.ServiceConfig
;
5 import org
.asamk
.signal
.manager
.storage
.SignalAccount
;
6 import org
.asamk
.signal
.manager
.storage
.recipients
.Profile
;
7 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientId
;
8 import org
.asamk
.signal
.manager
.util
.IOUtils
;
9 import org
.asamk
.signal
.manager
.util
.ProfileUtils
;
10 import org
.asamk
.signal
.manager
.util
.Utils
;
11 import org
.signal
.zkgroup
.profiles
.ProfileKey
;
12 import org
.signal
.zkgroup
.profiles
.ProfileKeyCredential
;
13 import org
.slf4j
.Logger
;
14 import org
.slf4j
.LoggerFactory
;
15 import org
.whispersystems
.libsignal
.IdentityKey
;
16 import org
.whispersystems
.libsignal
.InvalidKeyException
;
17 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
18 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccess
;
19 import org
.whispersystems
.signalservice
.api
.profiles
.ProfileAndCredential
;
20 import org
.whispersystems
.signalservice
.api
.profiles
.SignalServiceProfile
;
21 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
22 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NotFoundException
;
23 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.PushNetworkException
;
24 import org
.whispersystems
.signalservice
.api
.services
.ProfileService
;
27 import java
.io
.IOException
;
28 import java
.io
.OutputStream
;
29 import java
.nio
.file
.Files
;
30 import java
.util
.Base64
;
31 import java
.util
.Date
;
32 import java
.util
.List
;
33 import java
.util
.Objects
;
36 import io
.reactivex
.rxjava3
.core
.Maybe
;
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 Context context
;
47 public ProfileHelper(final Context context
) {
48 this.account
= context
.getAccount();
49 this.dependencies
= context
.getDependencies();
50 this.context
= context
;
53 public Profile
getRecipientProfile(RecipientId recipientId
) {
54 return getRecipientProfile(recipientId
, false);
57 public void refreshRecipientProfile(RecipientId recipientId
) {
58 getRecipientProfile(recipientId
, true);
61 public List
<ProfileKeyCredential
> getRecipientProfileKeyCredential(List
<RecipientId
> recipientIds
) {
62 final var profileFetches
= recipientIds
.stream().map(recipientId
-> {
63 var profileKeyCredential
= account
.getProfileStore().getProfileKeyCredential(recipientId
);
64 if (profileKeyCredential
!= null) {
68 return retrieveProfile(recipientId
,
69 SignalServiceProfile
.RequestType
.PROFILE_AND_CREDENTIAL
).onErrorComplete();
70 }).filter(Objects
::nonNull
).toList();
71 Maybe
.merge(profileFetches
).blockingSubscribe();
73 return recipientIds
.stream().map(r
-> account
.getProfileStore().getProfileKeyCredential(r
)).toList();
76 public ProfileKeyCredential
getRecipientProfileKeyCredential(RecipientId recipientId
) {
77 var profileKeyCredential
= account
.getProfileStore().getProfileKeyCredential(recipientId
);
78 if (profileKeyCredential
!= null) {
79 return profileKeyCredential
;
83 blockingGetProfile(retrieveProfile(recipientId
, SignalServiceProfile
.RequestType
.PROFILE_AND_CREDENTIAL
));
84 } catch (IOException e
) {
85 logger
.warn("Failed to retrieve profile key credential, ignoring: {}", e
.getMessage());
89 return account
.getProfileStore().getProfileKeyCredential(recipientId
);
93 * @param givenName if null, the previous givenName will be kept
94 * @param familyName if null, the previous familyName will be kept
95 * @param about if null, the previous about text will be kept
96 * @param aboutEmoji if null, the previous about emoji will be kept
97 * @param avatar if avatar is null the image from the local avatar store is used (if present),
99 public void setProfile(
100 String givenName
, final String familyName
, String about
, String aboutEmoji
, Optional
<File
> avatar
101 ) throws IOException
{
102 setProfile(true, givenName
, familyName
, about
, aboutEmoji
, avatar
);
105 public void setProfile(
106 boolean uploadProfile
,
108 final String familyName
,
111 Optional
<File
> avatar
112 ) throws IOException
{
113 var profile
= getRecipientProfile(account
.getSelfRecipientId());
114 var builder
= profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
);
115 if (givenName
!= null) {
116 builder
.withGivenName(givenName
);
118 if (familyName
!= null) {
119 builder
.withFamilyName(familyName
);
122 builder
.withAbout(about
);
124 if (aboutEmoji
!= null) {
125 builder
.withAboutEmoji(aboutEmoji
);
127 var newProfile
= builder
.build();
130 try (final var streamDetails
= avatar
== null
131 ? context
.getAvatarStore()
132 .retrieveProfileAvatar(account
.getSelfAddress())
133 : avatar
.isPresent() ? Utils
.createStreamDetailsFromFile(avatar
.get()) : null) {
134 final var avatarPath
= dependencies
.getAccountManager()
135 .setVersionedProfile(account
.getAci(),
136 account
.getProfileKey(),
137 newProfile
.getInternalServiceName(),
138 newProfile
.getAbout() == null ?
"" : newProfile
.getAbout(),
139 newProfile
.getAboutEmoji() == null ?
"" : newProfile
.getAboutEmoji(),
142 List
.of(/* TODO */));
143 builder
.withAvatarUrlPath(avatarPath
.orNull());
144 newProfile
= builder
.build();
148 if (avatar
!= null) {
149 if (avatar
.isPresent()) {
150 context
.getAvatarStore()
151 .storeProfileAvatar(account
.getSelfAddress(),
152 outputStream
-> IOUtils
.copyFileToStream(avatar
.get(), outputStream
));
154 context
.getAvatarStore().deleteProfileAvatar(account
.getSelfAddress());
157 account
.getProfileStore().storeProfile(account
.getSelfRecipientId(), newProfile
);
160 public List
<Profile
> getRecipientProfile(List
<RecipientId
> recipientIds
) {
161 final var profileFetches
= recipientIds
.stream().map(recipientId
-> {
162 var profile
= account
.getProfileStore().getProfile(recipientId
);
163 if (!isProfileRefreshRequired(profile
)) {
167 return retrieveProfile(recipientId
, SignalServiceProfile
.RequestType
.PROFILE
).onErrorComplete();
168 }).filter(Objects
::nonNull
).toList();
169 Maybe
.merge(profileFetches
).blockingSubscribe();
171 return recipientIds
.stream().map(r
-> account
.getProfileStore().getProfile(r
)).toList();
174 private Profile
getRecipientProfile(RecipientId recipientId
, boolean force
) {
175 var profile
= account
.getProfileStore().getProfile(recipientId
);
177 if (!force
&& !isProfileRefreshRequired(profile
)) {
182 blockingGetProfile(retrieveProfile(recipientId
, SignalServiceProfile
.RequestType
.PROFILE
));
183 } catch (IOException e
) {
184 logger
.warn("Failed to retrieve profile, ignoring: {}", e
.getMessage());
187 return account
.getProfileStore().getProfile(recipientId
);
190 private boolean isProfileRefreshRequired(final Profile profile
) {
191 if (profile
== null) {
194 // Profiles are cached for 6h before retrieving them again, unless forced
195 final var now
= System
.currentTimeMillis();
196 return now
- profile
.getLastUpdateTimestamp() >= 6 * 60 * 60 * 1000;
199 private SignalServiceProfile
retrieveProfileSync(String username
) throws IOException
{
200 final var locale
= Utils
.getDefaultLocale();
201 return dependencies
.getMessageReceiver().retrieveProfileByUsername(username
, Optional
.absent(), locale
);
204 private Profile
decryptProfileAndDownloadAvatar(
205 final RecipientId recipientId
, final ProfileKey profileKey
, final SignalServiceProfile encryptedProfile
207 final var avatarPath
= encryptedProfile
.getAvatar();
208 downloadProfileAvatar(recipientId
, avatarPath
, profileKey
);
210 return ProfileUtils
.decryptProfile(profileKey
, encryptedProfile
);
213 public void downloadProfileAvatar(
214 final RecipientId recipientId
, final String avatarPath
, final ProfileKey profileKey
216 var profile
= account
.getProfileStore().getProfile(recipientId
);
217 if (profile
== null || !Objects
.equals(avatarPath
, profile
.getAvatarUrlPath())) {
218 downloadProfileAvatar(context
.getRecipientHelper().resolveSignalServiceAddress(recipientId
),
221 var builder
= profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
);
222 account
.getProfileStore().storeProfile(recipientId
, builder
.withAvatarUrlPath(avatarPath
).build());
226 private ProfileAndCredential
blockingGetProfile(Single
<ProfileAndCredential
> profile
) throws IOException
{
228 return profile
.blockingGet();
229 } catch (RuntimeException e
) {
230 if (e
.getCause() instanceof PushNetworkException
) {
231 throw (PushNetworkException
) e
.getCause();
232 } else if (e
.getCause() instanceof NotFoundException
) {
233 throw (NotFoundException
) e
.getCause();
235 throw new IOException(e
);
240 private Single
<ProfileAndCredential
> retrieveProfile(
241 RecipientId recipientId
, SignalServiceProfile
.RequestType requestType
243 var unidentifiedAccess
= getUnidentifiedAccess(recipientId
);
244 var profileKey
= Optional
.fromNullable(account
.getProfileStore().getProfileKey(recipientId
));
246 final var address
= context
.getRecipientHelper().resolveSignalServiceAddress(recipientId
);
247 return retrieveProfile(address
, profileKey
, unidentifiedAccess
, requestType
).doOnSuccess(p
-> {
248 final var encryptedProfile
= p
.getProfile();
250 if (requestType
== SignalServiceProfile
.RequestType
.PROFILE_AND_CREDENTIAL
) {
251 final var profileKeyCredential
= p
.getProfileKeyCredential().orNull();
252 account
.getProfileStore().storeProfileKeyCredential(recipientId
, profileKeyCredential
);
255 final var profile
= account
.getProfileStore().getProfile(recipientId
);
257 Profile newProfile
= null;
258 if (profileKey
.isPresent()) {
259 newProfile
= decryptProfileAndDownloadAvatar(recipientId
, profileKey
.get(), encryptedProfile
);
262 if (newProfile
== null) {
264 profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
)
265 ).withLastUpdateTimestamp(System
.currentTimeMillis())
266 .withUnidentifiedAccessMode(ProfileUtils
.getUnidentifiedAccessMode(encryptedProfile
, null))
267 .withCapabilities(ProfileUtils
.getCapabilities(encryptedProfile
))
271 account
.getProfileStore().storeProfile(recipientId
, newProfile
);
274 var newIdentity
= account
.getIdentityKeyStore()
275 .saveIdentity(recipientId
,
276 new IdentityKey(Base64
.getDecoder().decode(encryptedProfile
.getIdentityKey())),
280 account
.getSessionStore().archiveSessions(recipientId
);
281 account
.getSenderKeyStore().deleteSharedWith(recipientId
);
283 } catch (InvalidKeyException ignored
) {
284 logger
.warn("Got invalid identity key in profile for {}",
285 context
.getRecipientHelper().resolveSignalServiceAddress(recipientId
).getIdentifier());
288 logger
.warn("Failed to retrieve profile, ignoring: {}", e
.getMessage());
289 final var profile
= account
.getProfileStore().getProfile(recipientId
);
290 final var newProfile
= (
291 profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
)
292 ).withLastUpdateTimestamp(System
.currentTimeMillis())
293 .withUnidentifiedAccessMode(Profile
.UnidentifiedAccessMode
.UNKNOWN
)
294 .withCapabilities(Set
.of())
297 account
.getProfileStore().storeProfile(recipientId
, newProfile
);
301 private Single
<ProfileAndCredential
> retrieveProfile(
302 SignalServiceAddress address
,
303 Optional
<ProfileKey
> profileKey
,
304 Optional
<UnidentifiedAccess
> unidentifiedAccess
,
305 SignalServiceProfile
.RequestType requestType
307 final var profileService
= dependencies
.getProfileService();
308 final var locale
= Utils
.getDefaultLocale();
310 return profileService
.getProfile(address
, profileKey
, unidentifiedAccess
, requestType
, locale
).map(pair
-> {
311 var processor
= new ProfileService
.ProfileResponseProcessor(pair
);
312 if (processor
.hasResult()) {
313 return processor
.getResult();
314 } else if (processor
.notFound()) {
315 throw new NotFoundException("Profile not found");
317 throw pair
.getExecutionError()
318 .or(pair
.getApplicationError())
319 .or(new IOException("Unknown error while retrieving profile"));
324 private void downloadProfileAvatar(
325 SignalServiceAddress address
, String avatarPath
, ProfileKey profileKey
327 if (avatarPath
== null) {
329 context
.getAvatarStore().deleteProfileAvatar(address
);
330 } catch (IOException e
) {
331 logger
.warn("Failed to delete local profile avatar, ignoring: {}", e
.getMessage());
337 context
.getAvatarStore()
338 .storeProfileAvatar(address
,
339 outputStream
-> retrieveProfileAvatar(avatarPath
, profileKey
, outputStream
));
340 } catch (Throwable e
) {
341 if (e
instanceof AssertionError
&& e
.getCause() instanceof InterruptedException
) {
342 Thread
.currentThread().interrupt();
344 logger
.warn("Failed to download profile avatar, ignoring: {}", e
.getMessage());
348 private void retrieveProfileAvatar(
349 String avatarPath
, ProfileKey profileKey
, OutputStream outputStream
350 ) throws IOException
{
351 var tmpFile
= IOUtils
.createTempFile();
352 try (var input
= dependencies
.getMessageReceiver()
353 .retrieveProfileAvatar(avatarPath
,
356 ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
)) {
357 // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
358 IOUtils
.copyStream(input
, outputStream
, (int) ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
);
361 Files
.delete(tmpFile
.toPath());
362 } catch (IOException e
) {
363 logger
.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}",
370 private Optional
<UnidentifiedAccess
> getUnidentifiedAccess(RecipientId recipientId
) {
371 var unidentifiedAccess
= context
.getUnidentifiedAccessHelper().getAccessFor(recipientId
, true);
373 if (unidentifiedAccess
.isPresent()) {
374 return unidentifiedAccess
.get().getTargetUnidentifiedAccess();
377 return Optional
.absent();