]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java
Create libsignal dependencies only when required
[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 SignalServiceAddressResolver addressResolver;
49
50 public ProfileHelper(
51 final SignalAccount account,
52 final SignalDependencies dependencies,
53 final AvatarStore avatarStore,
54 final ProfileKeyProvider profileKeyProvider,
55 final UnidentifiedAccessProvider unidentifiedAccessProvider,
56 final SignalServiceAddressResolver addressResolver
57 ) {
58 this.account = account;
59 this.dependencies = dependencies;
60 this.avatarStore = avatarStore;
61 this.profileKeyProvider = profileKeyProvider;
62 this.unidentifiedAccessProvider = unidentifiedAccessProvider;
63 this.addressResolver = addressResolver;
64 }
65
66 public Profile getRecipientProfile(RecipientId recipientId) {
67 return getRecipientProfile(recipientId, false);
68 }
69
70 public void refreshRecipientProfile(RecipientId recipientId) {
71 getRecipientProfile(recipientId, true);
72 }
73
74 public ProfileKeyCredential getRecipientProfileKeyCredential(RecipientId recipientId) {
75 var profileKeyCredential = account.getProfileStore().getProfileKeyCredential(recipientId);
76 if (profileKeyCredential != null) {
77 return profileKeyCredential;
78 }
79
80 ProfileAndCredential profileAndCredential;
81 try {
82 profileAndCredential = retrieveProfileAndCredential(recipientId,
83 SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL);
84 } catch (IOException e) {
85 logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage());
86 return null;
87 }
88
89 profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull();
90 account.getProfileStore().storeProfileKeyCredential(recipientId, profileKeyCredential);
91
92 var profileKey = account.getProfileStore().getProfileKey(recipientId);
93 if (profileKey != null) {
94 final var profile = decryptProfileAndDownloadAvatar(recipientId,
95 profileKey,
96 profileAndCredential.getProfile());
97 account.getProfileStore().storeProfile(recipientId, profile);
98 }
99
100 return profileKeyCredential;
101 }
102
103 /**
104 * @param givenName if null, the previous givenName will be kept
105 * @param familyName if null, the previous familyName will be kept
106 * @param about if null, the previous about text will be kept
107 * @param aboutEmoji if null, the previous about emoji will be kept
108 * @param avatar if avatar is null the image from the local avatar store is used (if present),
109 */
110 public void setProfile(
111 String givenName, final String familyName, String about, String aboutEmoji, 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);
117 }
118 if (familyName != null) {
119 builder.withFamilyName(familyName);
120 }
121 if (about != null) {
122 builder.withAbout(about);
123 }
124 if (aboutEmoji != null) {
125 builder.withAboutEmoji(aboutEmoji);
126 }
127 var newProfile = builder.build();
128
129 try (final var streamDetails = avatar == null
130 ? avatarStore.retrieveProfileAvatar(account.getSelfAddress())
131 : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) {
132 dependencies.getAccountManager()
133 .setVersionedProfile(account.getUuid(),
134 account.getProfileKey(),
135 newProfile.getInternalServiceName(),
136 newProfile.getAbout() == null ? "" : newProfile.getAbout(),
137 newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(),
138 Optional.absent(),
139 streamDetails);
140 }
141
142 if (avatar != null) {
143 if (avatar.isPresent()) {
144 avatarStore.storeProfileAvatar(account.getSelfAddress(),
145 outputStream -> IOUtils.copyFileToStream(avatar.get(), outputStream));
146 } else {
147 avatarStore.deleteProfileAvatar(account.getSelfAddress());
148 }
149 }
150 account.getProfileStore().storeProfile(account.getSelfRecipientId(), newProfile);
151 }
152
153 private final Set<RecipientId> pendingProfileRequest = new HashSet<>();
154
155 private Profile getRecipientProfile(RecipientId recipientId, boolean force) {
156 var profile = account.getProfileStore().getProfile(recipientId);
157
158 var now = System.currentTimeMillis();
159 // Profiles are cached for 24h before retrieving them again, unless forced
160 if (!force && profile != null && now - profile.getLastUpdateTimestamp() < 24 * 60 * 60 * 1000) {
161 return profile;
162 }
163
164 synchronized (pendingProfileRequest) {
165 if (pendingProfileRequest.contains(recipientId)) {
166 return profile;
167 }
168 pendingProfileRequest.add(recipientId);
169 }
170 final SignalServiceProfile encryptedProfile;
171 try {
172 encryptedProfile = retrieveEncryptedProfile(recipientId);
173 } finally {
174 synchronized (pendingProfileRequest) {
175 pendingProfileRequest.remove(recipientId);
176 }
177 }
178 if (encryptedProfile == null) {
179 return null;
180 }
181
182 profile = decryptProfileIfKeyKnown(recipientId, encryptedProfile);
183 account.getProfileStore().storeProfile(recipientId, profile);
184
185 return profile;
186 }
187
188 private Profile decryptProfileIfKeyKnown(
189 final RecipientId recipientId, final SignalServiceProfile encryptedProfile
190 ) {
191 var profileKey = account.getProfileStore().getProfileKey(recipientId);
192 if (profileKey == null) {
193 return new Profile(System.currentTimeMillis(),
194 null,
195 null,
196 null,
197 null,
198 ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null),
199 ProfileUtils.getCapabilities(encryptedProfile));
200 }
201
202 return decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile);
203 }
204
205 private SignalServiceProfile retrieveEncryptedProfile(RecipientId recipientId) {
206 try {
207 return retrieveProfileAndCredential(recipientId, SignalServiceProfile.RequestType.PROFILE).getProfile();
208 } catch (IOException e) {
209 logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
210 return null;
211 }
212 }
213
214 private SignalServiceProfile retrieveProfileSync(String username) throws IOException {
215 return dependencies.getMessageReceiver().retrieveProfileByUsername(username, Optional.absent());
216 }
217
218 private ProfileAndCredential retrieveProfileAndCredential(
219 final RecipientId recipientId, final SignalServiceProfile.RequestType requestType
220 ) throws IOException {
221 final var profileAndCredential = retrieveProfileSync(recipientId, requestType);
222 final var profile = profileAndCredential.getProfile();
223
224 try {
225 var newIdentity = account.getIdentityKeyStore()
226 .saveIdentity(recipientId,
227 new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())),
228 new Date());
229
230 if (newIdentity) {
231 account.getSessionStore().archiveSessions(recipientId);
232 }
233 } catch (InvalidKeyException ignored) {
234 logger.warn("Got invalid identity key in profile for {}",
235 addressResolver.resolveSignalServiceAddress(recipientId).getIdentifier());
236 }
237 return profileAndCredential;
238 }
239
240 private Profile decryptProfileAndDownloadAvatar(
241 final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
242 ) {
243 if (encryptedProfile.getAvatar() != null) {
244 downloadProfileAvatar(addressResolver.resolveSignalServiceAddress(recipientId),
245 encryptedProfile.getAvatar(),
246 profileKey);
247 }
248
249 return ProfileUtils.decryptProfile(profileKey, encryptedProfile);
250 }
251
252 private ProfileAndCredential retrieveProfileSync(
253 RecipientId recipientId, SignalServiceProfile.RequestType requestType
254 ) throws IOException {
255 try {
256 return retrieveProfile(recipientId, requestType).blockingGet();
257 } catch (RuntimeException e) {
258 if (e.getCause() instanceof PushNetworkException) {
259 throw (PushNetworkException) e.getCause();
260 } else if (e.getCause() instanceof NotFoundException) {
261 throw (NotFoundException) e.getCause();
262 } else {
263 throw new IOException(e);
264 }
265 }
266 }
267
268 private Single<ProfileAndCredential> retrieveProfile(
269 RecipientId recipientId, SignalServiceProfile.RequestType requestType
270 ) {
271 var unidentifiedAccess = getUnidentifiedAccess(recipientId);
272 var profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(recipientId));
273
274 final var address = addressResolver.resolveSignalServiceAddress(recipientId);
275 return retrieveProfile(address, profileKey, unidentifiedAccess, requestType);
276 }
277
278 private Single<ProfileAndCredential> retrieveProfile(
279 SignalServiceAddress address,
280 Optional<ProfileKey> profileKey,
281 Optional<UnidentifiedAccess> unidentifiedAccess,
282 SignalServiceProfile.RequestType requestType
283 ) {
284 var profileService = dependencies.getProfileService();
285
286 Single<ServiceResponse<ProfileAndCredential>> responseSingle;
287 try {
288 responseSingle = profileService.getProfile(address, profileKey, unidentifiedAccess, requestType);
289 } catch (NoClassDefFoundError e) {
290 // Native zkgroup lib not available for ProfileKey
291 responseSingle = profileService.getProfile(address, Optional.absent(), unidentifiedAccess, requestType);
292 }
293
294 return responseSingle.map(pair -> {
295 var processor = new ProfileService.ProfileResponseProcessor(pair);
296 if (processor.hasResult()) {
297 return processor.getResult();
298 } else if (processor.notFound()) {
299 throw new NotFoundException("Profile not found");
300 } else {
301 throw pair.getExecutionError()
302 .or(pair.getApplicationError())
303 .or(new IOException("Unknown error while retrieving profile"));
304 }
305 });
306 }
307
308 private void downloadProfileAvatar(
309 SignalServiceAddress address, String avatarPath, ProfileKey profileKey
310 ) {
311 try {
312 avatarStore.storeProfileAvatar(address,
313 outputStream -> retrieveProfileAvatar(avatarPath, profileKey, outputStream));
314 } catch (Throwable e) {
315 if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) {
316 Thread.currentThread().interrupt();
317 }
318 logger.warn("Failed to download profile avatar, ignoring: {}", e.getMessage());
319 }
320 }
321
322 private void retrieveProfileAvatar(
323 String avatarPath, ProfileKey profileKey, OutputStream outputStream
324 ) throws IOException {
325 var tmpFile = IOUtils.createTempFile();
326 try (var input = dependencies.getMessageReceiver()
327 .retrieveProfileAvatar(avatarPath,
328 tmpFile,
329 profileKey,
330 ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
331 // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
332 IOUtils.copyStream(input, outputStream, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE);
333 } finally {
334 try {
335 Files.delete(tmpFile.toPath());
336 } catch (IOException e) {
337 logger.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}",
338 tmpFile,
339 e.getMessage());
340 }
341 }
342 }
343
344 private Optional<UnidentifiedAccess> getUnidentifiedAccess(RecipientId recipientId) {
345 var unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipientId);
346
347 if (unidentifiedAccess.isPresent()) {
348 return unidentifiedAccess.get().getTargetUnidentifiedAccess();
349 }
350
351 return Optional.absent();
352 }
353 }