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