]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java
Update libsignal-service-java
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / ProfileHelper.java
1 package org.asamk.signal.manager.helper;
2
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;
27
28 import java.io.File;
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;
35 import java.util.Set;
36
37 import io.reactivex.rxjava3.core.Single;
38
39 public final class ProfileHelper {
40
41 private final static Logger logger = LoggerFactory.getLogger(ProfileHelper.class);
42
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;
51
52 public ProfileHelper(
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
61 ) {
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;
70 }
71
72 public Profile getRecipientProfile(RecipientId recipientId) {
73 return getRecipientProfile(recipientId, false);
74 }
75
76 public void refreshRecipientProfile(RecipientId recipientId) {
77 getRecipientProfile(recipientId, true);
78 }
79
80 public ProfileKeyCredential getRecipientProfileKeyCredential(RecipientId recipientId) {
81 var profileKeyCredential = account.getProfileStore().getProfileKeyCredential(recipientId);
82 if (profileKeyCredential != null) {
83 return profileKeyCredential;
84 }
85
86 ProfileAndCredential profileAndCredential;
87 try {
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());
92 return null;
93 }
94
95 profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull();
96 account.getProfileStore().storeProfileKeyCredential(recipientId, profileKeyCredential);
97
98 var profileKey = account.getProfileStore().getProfileKey(recipientId);
99 if (profileKey != null) {
100 final var profile = decryptProfileAndDownloadAvatar(recipientId,
101 profileKey,
102 profileAndCredential.getProfile());
103 account.getProfileStore().storeProfile(recipientId, profile);
104 }
105
106 return profileKeyCredential;
107 }
108
109 /**
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),
115 */
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);
123 }
124 if (familyName != null) {
125 builder.withFamilyName(familyName);
126 }
127 if (about != null) {
128 builder.withAbout(about);
129 }
130 if (aboutEmoji != null) {
131 builder.withAboutEmoji(aboutEmoji);
132 }
133 var newProfile = builder.build();
134
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(),
144 Optional.absent(),
145 streamDetails);
146 }
147
148 if (avatar != null) {
149 if (avatar.isPresent()) {
150 avatarStore.storeProfileAvatar(account.getSelfAddress(),
151 outputStream -> IOUtils.copyFileToStream(avatar.get(), outputStream));
152 } else {
153 avatarStore.deleteProfileAvatar(account.getSelfAddress());
154 }
155 }
156 account.getProfileStore().storeProfile(account.getSelfRecipientId(), newProfile);
157 }
158
159 private final Set<RecipientId> pendingProfileRequest = new HashSet<>();
160
161 private Profile getRecipientProfile(RecipientId recipientId, boolean force) {
162 var profile = account.getProfileStore().getProfile(recipientId);
163
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) {
167 return profile;
168 }
169
170 synchronized (pendingProfileRequest) {
171 if (pendingProfileRequest.contains(recipientId)) {
172 return profile;
173 }
174 pendingProfileRequest.add(recipientId);
175 }
176 final SignalServiceProfile encryptedProfile;
177 try {
178 encryptedProfile = retrieveEncryptedProfile(recipientId);
179 } finally {
180 synchronized (pendingProfileRequest) {
181 pendingProfileRequest.remove(recipientId);
182 }
183 }
184 if (encryptedProfile == null) {
185 return null;
186 }
187
188 profile = decryptProfileIfKeyKnown(recipientId, encryptedProfile);
189 account.getProfileStore().storeProfile(recipientId, profile);
190
191 return profile;
192 }
193
194 private Profile decryptProfileIfKeyKnown(
195 final RecipientId recipientId, final SignalServiceProfile encryptedProfile
196 ) {
197 var profileKey = account.getProfileStore().getProfileKey(recipientId);
198 if (profileKey == null) {
199 return new Profile(System.currentTimeMillis(),
200 null,
201 null,
202 null,
203 null,
204 ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null),
205 ProfileUtils.getCapabilities(encryptedProfile));
206 }
207
208 return decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile);
209 }
210
211 private SignalServiceProfile retrieveEncryptedProfile(RecipientId recipientId) {
212 try {
213 return retrieveProfileAndCredential(recipientId, SignalServiceProfile.RequestType.PROFILE).getProfile();
214 } catch (IOException e) {
215 logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
216 return null;
217 }
218 }
219
220 private SignalServiceProfile retrieveProfileSync(String username) throws IOException {
221 return messageReceiverProvider.getMessageReceiver().retrieveProfileByUsername(username, Optional.absent());
222 }
223
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();
229
230 try {
231 var newIdentity = account.getIdentityKeyStore()
232 .saveIdentity(recipientId,
233 new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())),
234 new Date());
235
236 if (newIdentity) {
237 account.getSessionStore().archiveSessions(recipientId);
238 }
239 } catch (InvalidKeyException ignored) {
240 logger.warn("Got invalid identity key in profile for {}",
241 addressResolver.resolveSignalServiceAddress(recipientId).getIdentifier());
242 }
243 return profileAndCredential;
244 }
245
246 private Profile decryptProfileAndDownloadAvatar(
247 final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
248 ) {
249 if (encryptedProfile.getAvatar() != null) {
250 downloadProfileAvatar(addressResolver.resolveSignalServiceAddress(recipientId),
251 encryptedProfile.getAvatar(),
252 profileKey);
253 }
254
255 return ProfileUtils.decryptProfile(profileKey, encryptedProfile);
256 }
257
258 private ProfileAndCredential retrieveProfileSync(
259 RecipientId recipientId, SignalServiceProfile.RequestType requestType
260 ) throws IOException {
261 try {
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();
268 } else {
269 throw new IOException(e);
270 }
271 }
272 }
273
274 private Single<ProfileAndCredential> retrieveProfile(
275 RecipientId recipientId, SignalServiceProfile.RequestType requestType
276 ) {
277 var unidentifiedAccess = getUnidentifiedAccess(recipientId);
278 var profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(recipientId));
279
280 final var address = addressResolver.resolveSignalServiceAddress(recipientId);
281 return retrieveProfile(address, profileKey, unidentifiedAccess, requestType);
282 }
283
284 private Single<ProfileAndCredential> retrieveProfile(
285 SignalServiceAddress address,
286 Optional<ProfileKey> profileKey,
287 Optional<UnidentifiedAccess> unidentifiedAccess,
288 SignalServiceProfile.RequestType requestType
289 ) {
290 var profileService = profileServiceProvider.getProfileService();
291
292 Single<ServiceResponse<ProfileAndCredential>> responseSingle;
293 try {
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);
298 }
299
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");
306 } else {
307 throw pair.getExecutionError()
308 .or(pair.getApplicationError())
309 .or(new IOException("Unknown error while retrieving profile"));
310 }
311 });
312 }
313
314 private void downloadProfileAvatar(
315 SignalServiceAddress address, String avatarPath, ProfileKey profileKey
316 ) {
317 try {
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();
323 }
324 logger.warn("Failed to download profile avatar, ignoring: {}", e.getMessage());
325 }
326 }
327
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,
334 tmpFile,
335 profileKey,
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);
339 } finally {
340 try {
341 Files.delete(tmpFile.toPath());
342 } catch (IOException e) {
343 logger.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}",
344 tmpFile,
345 e.getMessage());
346 }
347 }
348 }
349
350 private Optional<UnidentifiedAccess> getUnidentifiedAccess(RecipientId recipientId) {
351 var unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipientId);
352
353 if (unidentifiedAccess.isPresent()) {
354 return unidentifiedAccess.get().getTargetUnidentifiedAccess();
355 }
356
357 return Optional.absent();
358 }
359 }