That fuck shit the fascists are using
1package org.tm.archive.util;
2
3import android.content.Context;
4
5import androidx.annotation.NonNull;
6import androidx.annotation.Nullable;
7import androidx.annotation.WorkerThread;
8
9import org.signal.core.util.Base64;
10import org.signal.core.util.logging.Log;
11import org.signal.libsignal.protocol.IdentityKey;
12import org.signal.libsignal.protocol.IdentityKeyPair;
13import org.signal.libsignal.protocol.InvalidKeyException;
14import org.signal.libsignal.protocol.util.Pair;
15import org.signal.libsignal.zkgroup.InvalidInputException;
16import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
17import org.signal.libsignal.zkgroup.profiles.ProfileKey;
18import org.tm.archive.badges.models.Badge;
19import org.tm.archive.crypto.ProfileKeyUtil;
20import org.tm.archive.crypto.UnidentifiedAccessUtil;
21import org.tm.archive.database.RecipientTable;
22import org.tm.archive.database.SignalDatabase;
23import org.tm.archive.dependencies.ApplicationDependencies;
24import org.tm.archive.jobmanager.Job;
25import org.tm.archive.jobs.GroupV2UpdateSelfProfileKeyJob;
26import org.tm.archive.jobs.MultiDeviceProfileKeyUpdateJob;
27import org.tm.archive.jobs.ProfileUploadJob;
28import org.tm.archive.jobs.RefreshAttributesJob;
29import org.tm.archive.jobs.RefreshOwnProfileJob;
30import org.tm.archive.keyvalue.SignalStore;
31import org.tm.archive.payments.MobileCoinPublicAddress;
32import org.tm.archive.payments.MobileCoinPublicAddressProfileUtil;
33import org.tm.archive.payments.PaymentsAddressException;
34import org.tm.archive.profiles.AvatarHelper;
35import org.tm.archive.profiles.ProfileName;
36import org.tm.archive.recipients.Recipient;
37import org.tm.archive.recipients.RecipientUtil;
38import org.whispersystems.signalservice.api.SignalServiceAccountManager;
39import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
40import org.whispersystems.signalservice.api.crypto.ProfileCipher;
41import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
42import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
43import org.whispersystems.signalservice.api.profiles.AvatarUploadParams;
44import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
45import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
46import org.whispersystems.signalservice.api.push.SignalServiceAddress;
47import org.whispersystems.signalservice.api.services.ProfileService;
48import org.whispersystems.signalservice.api.util.StreamDetails;
49import org.whispersystems.signalservice.internal.ServiceResponse;
50import org.whispersystems.signalservice.internal.push.PaymentAddress;
51
52import java.io.IOException;
53import java.util.List;
54import java.util.Locale;
55import java.util.Optional;
56import java.util.stream.Collectors;
57
58import io.reactivex.rxjava3.core.Single;
59
60/**
61 * Aids in the retrieval and decryption of profiles.
62 */
63public final class ProfileUtil {
64
65 private static final String TAG = Log.tag(ProfileUtil.class);
66
67 private ProfileUtil() {
68 }
69
70 /**
71 * Should be called after a change to our own profile key as been persisted to the database.
72 */
73 @WorkerThread
74 public static void handleSelfProfileKeyChange() {
75 List<Job> gv2UpdateJobs = SignalDatabase.groups()
76 .getAllGroupV2Ids()
77 .stream()
78 .map(GroupV2UpdateSelfProfileKeyJob::withoutLimits)
79 .collect(Collectors.toList());
80
81 Log.w(TAG, "[handleSelfProfileKeyChange] Scheduling jobs, including " + gv2UpdateJobs.size() + " group update jobs.");
82
83 ApplicationDependencies.getJobManager()
84 .startChain(new RefreshAttributesJob())
85 .then(new ProfileUploadJob())
86 .then(new MultiDeviceProfileKeyUpdateJob())
87 .then(gv2UpdateJobs)
88 .enqueue();
89 }
90
91 @WorkerThread
92 public static @NonNull ProfileAndCredential retrieveProfileSync(@NonNull Context context,
93 @NonNull Recipient recipient,
94 @NonNull SignalServiceProfile.RequestType requestType)
95 throws IOException
96 {
97 return retrieveProfileSync(context, recipient, requestType, true);
98 }
99
100 @WorkerThread
101 public static @NonNull ProfileAndCredential retrieveProfileSync(@NonNull Context context,
102 @NonNull Recipient recipient,
103 @NonNull SignalServiceProfile.RequestType requestType,
104 boolean allowUnidentifiedAccess)
105 throws IOException
106 {
107 Pair<Recipient, ServiceResponse<ProfileAndCredential>> response = retrieveProfile(context, recipient, requestType, allowUnidentifiedAccess).blockingGet();
108 return new ProfileService.ProfileResponseProcessor(response.second()).getResultOrThrow();
109 }
110
111 public static Single<Pair<Recipient, ServiceResponse<ProfileAndCredential>>> retrieveProfile(@NonNull Context context,
112 @NonNull Recipient recipient,
113 @NonNull SignalServiceProfile.RequestType requestType)
114 {
115 return retrieveProfile(context, recipient, requestType, true);
116 }
117
118 private static Single<Pair<Recipient, ServiceResponse<ProfileAndCredential>>> retrieveProfile(@NonNull Context context,
119 @NonNull Recipient recipient,
120 @NonNull SignalServiceProfile.RequestType requestType,
121 boolean allowUnidentifiedAccess)
122 {
123 ProfileService profileService = ApplicationDependencies.getProfileService();
124 Optional<UnidentifiedAccess> unidentifiedAccess = allowUnidentifiedAccess ? getUnidentifiedAccess(context, recipient) : Optional.empty();
125 Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey());
126
127 return Single.fromCallable(() -> toSignalServiceAddress(context, recipient))
128 .flatMap(address -> profileService.getProfile(address, profileKey, unidentifiedAccess, requestType, Locale.getDefault()).map(p -> new Pair<>(recipient, p)))
129 .onErrorReturn(t -> new Pair<>(recipient, ServiceResponse.forUnknownError(t)));
130 }
131
132 public static @Nullable String decryptString(@NonNull ProfileKey profileKey, @Nullable byte[] encryptedString)
133 throws InvalidCiphertextException, IOException
134 {
135 if (encryptedString == null) {
136 return null;
137 }
138
139 ProfileCipher profileCipher = new ProfileCipher(profileKey);
140 return profileCipher.decryptString(encryptedString);
141 }
142
143 public static @Nullable String decryptString(@NonNull ProfileKey profileKey, @Nullable String encryptedStringBase64)
144 throws InvalidCiphertextException, IOException
145 {
146 if (encryptedStringBase64 == null) {
147 return null;
148 }
149
150 return decryptString(profileKey, Base64.decode(encryptedStringBase64));
151 }
152
153 public static Optional<Boolean> decryptBoolean(@NonNull ProfileKey profileKey, @Nullable String encryptedBooleanBase64)
154 throws InvalidCiphertextException, IOException
155 {
156 if (encryptedBooleanBase64 == null) {
157 return Optional.empty();
158 }
159
160 ProfileCipher profileCipher = new ProfileCipher(profileKey);
161 return profileCipher.decryptBoolean(Base64.decode(encryptedBooleanBase64));
162 }
163
164 @WorkerThread
165 public static @NonNull MobileCoinPublicAddress getAddressForRecipient(@NonNull Recipient recipient)
166 throws IOException, PaymentsAddressException
167 {
168 ProfileKey profileKey;
169 try {
170 profileKey = getProfileKey(recipient);
171 } catch (IOException e) {
172 Log.w(TAG, "Profile key not available for " + recipient.getId());
173 throw new PaymentsAddressException(PaymentsAddressException.Code.NO_PROFILE_KEY);
174 }
175 ProfileAndCredential profileAndCredential = ProfileUtil.retrieveProfileSync(ApplicationDependencies.getApplication(), recipient, SignalServiceProfile.RequestType.PROFILE);
176 SignalServiceProfile profile = profileAndCredential.getProfile();
177 byte[] encryptedPaymentsAddress = profile.getPaymentAddress();
178
179 if (encryptedPaymentsAddress == null) {
180 Log.w(TAG, "Payments not enabled for " + recipient.getId());
181 throw new PaymentsAddressException(PaymentsAddressException.Code.NOT_ENABLED);
182 }
183
184 try {
185 IdentityKey identityKey = new IdentityKey(Base64.decode(profileAndCredential.getProfile().getIdentityKey()), 0);
186 ProfileCipher profileCipher = new ProfileCipher(profileKey);
187 byte[] decrypted = profileCipher.decryptWithLength(encryptedPaymentsAddress);
188 PaymentAddress paymentAddress = PaymentAddress.ADAPTER.decode(decrypted);
189 byte[] bytes = MobileCoinPublicAddressProfileUtil.verifyPaymentsAddress(paymentAddress, identityKey);
190 MobileCoinPublicAddress mobileCoinPublicAddress = MobileCoinPublicAddress.fromBytes(bytes);
191
192 if (mobileCoinPublicAddress == null) {
193 throw new PaymentsAddressException(PaymentsAddressException.Code.INVALID_ADDRESS);
194 }
195
196 return mobileCoinPublicAddress;
197 } catch (InvalidCiphertextException | IOException e) {
198 Log.w(TAG, "Could not decrypt payments address, ProfileKey may be outdated for " + recipient.getId(), e);
199 throw new PaymentsAddressException(PaymentsAddressException.Code.COULD_NOT_DECRYPT);
200 } catch (InvalidKeyException e) {
201 Log.w(TAG, "Could not verify payments address due to bad identity key " + recipient.getId(), e);
202 throw new PaymentsAddressException(PaymentsAddressException.Code.INVALID_ADDRESS_SIGNATURE);
203 }
204 }
205
206 private static ProfileKey getProfileKey(@NonNull Recipient recipient) throws IOException {
207 byte[] profileKeyBytes = recipient.getProfileKey();
208
209 if (profileKeyBytes == null) {
210 Log.w(TAG, "Profile key unknown for " + recipient.getId());
211 throw new IOException("No profile key");
212 }
213
214 ProfileKey profileKey;
215 try {
216 profileKey = new ProfileKey(profileKeyBytes);
217 } catch (InvalidInputException e) {
218 Log.w(TAG, "Profile key invalid for " + recipient.getId());
219 throw new IOException("Invalid profile key");
220 }
221 return profileKey;
222 }
223
224 /**
225 * Uploads the profile based on all state that's written to disk, except we'll use the provided
226 * list of badges instead. This is useful when you want to ensure that the profile has been uploaded
227 * successfully before persisting the change to disk.
228 */
229 public static void uploadProfileWithBadges(@NonNull Context context, @NonNull List<Badge> badges) throws IOException {
230 Log.d(TAG, "uploadProfileWithBadges()");
231 uploadProfile(Recipient.self().getProfileName(),
232 Optional.ofNullable(Recipient.self().getAbout()).orElse(""),
233 Optional.ofNullable(Recipient.self().getAboutEmoji()).orElse(""),
234 getSelfPaymentsAddressProtobuf(),
235 AvatarUploadParams.unchanged(AvatarHelper.hasAvatar(context, Recipient.self().getId())),
236 badges);
237 }
238
239 /**
240 * Uploads the profile based on all state that's written to disk, except we'll use the provided
241 * profile name instead. This is useful when you want to ensure that the profile has been uploaded
242 * successfully before persisting the change to disk.
243 */
244 public static void uploadProfileWithName(@NonNull Context context, @NonNull ProfileName profileName) throws IOException {
245 Log.d(TAG, "uploadProfileWithName()");
246 try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) {
247 uploadProfile(profileName,
248 Optional.ofNullable(Recipient.self().getAbout()).orElse(""),
249 Optional.ofNullable(Recipient.self().getAboutEmoji()).orElse(""),
250 getSelfPaymentsAddressProtobuf(),
251 AvatarUploadParams.unchanged(AvatarHelper.hasAvatar(context, Recipient.self().getId())),
252 Recipient.self().getBadges());
253 }
254 }
255
256 /**
257 * Uploads the profile based on all state that's written to disk, except we'll use the provided
258 * about/emoji instead. This is useful when you want to ensure that the profile has been uploaded
259 * successfully before persisting the change to disk.
260 */
261 public static void uploadProfileWithAbout(@NonNull Context context, @NonNull String about, @NonNull String emoji) throws IOException {
262 Log.d(TAG, "uploadProfileWithAbout()");
263 try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) {
264 uploadProfile(Recipient.self().getProfileName(),
265 about,
266 emoji,
267 getSelfPaymentsAddressProtobuf(),
268 AvatarUploadParams.unchanged(AvatarHelper.hasAvatar(context, Recipient.self().getId())),
269 Recipient.self().getBadges());
270 }
271 }
272
273 /**
274 * Uploads the profile based on all state that's already written to disk.
275 */
276 public static void uploadProfile(@NonNull Context context) throws IOException {
277 Log.d(TAG, "uploadProfile()");
278 try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) {
279 uploadProfileWithAvatar(avatar);
280 }
281 }
282
283 /**
284 * Uploads the profile based on all state that's written to disk, except we'll use the provided
285 * avatar instead. This is useful when you want to ensure that the profile has been uploaded
286 * successfully before persisting the change to disk.
287 */
288 public static void uploadProfileWithAvatar(@Nullable StreamDetails avatar) throws IOException {
289 Log.d(TAG, "uploadProfileWithAvatar()");
290 uploadProfile(Recipient.self().getProfileName(),
291 Optional.ofNullable(Recipient.self().getAbout()).orElse(""),
292 Optional.ofNullable(Recipient.self().getAboutEmoji()).orElse(""),
293 getSelfPaymentsAddressProtobuf(),
294 AvatarUploadParams.forAvatar(avatar),
295 Recipient.self().getBadges());
296 }
297
298 /**
299 * Attempts to update just the expiring profile key credential with a new one. If unable, an empty optional is returned.
300 *
301 * Note: It will try to find missing profile key credentials from the server and persist locally.
302 */
303 public static Optional<ExpiringProfileKeyCredential> updateExpiringProfileKeyCredential(@NonNull Recipient recipient) throws IOException {
304 ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
305
306 if (profileKey != null) {
307 Log.i(TAG, String.format("Updating profile key credential on recipient %s, fetching", recipient.getId()));
308
309 Optional<ExpiringProfileKeyCredential> profileKeyCredentialOptional = ApplicationDependencies.getSignalServiceAccountManager()
310 .resolveProfileKeyCredential(recipient.requireAci(), profileKey, Locale.getDefault());
311
312 if (profileKeyCredentialOptional.isPresent()) {
313 boolean updatedProfileKey = SignalDatabase.recipients().setProfileKeyCredential(recipient.getId(), profileKey, profileKeyCredentialOptional.get());
314
315 if (!updatedProfileKey) {
316 Log.w(TAG, String.format("Failed to update the profile key credential on recipient %s", recipient.getId()));
317 } else {
318 Log.i(TAG, String.format("Got new profile key credential for recipient %s", recipient.getId()));
319 return profileKeyCredentialOptional;
320 }
321 }
322 }
323
324 return Optional.empty();
325 }
326
327 private static void uploadProfile(@NonNull ProfileName profileName,
328 @Nullable String about,
329 @Nullable String aboutEmoji,
330 @Nullable PaymentAddress paymentsAddress,
331 @NonNull AvatarUploadParams avatar,
332 @NonNull List<Badge> badges)
333 throws IOException
334 {
335 List<String> badgeIds = badges.stream()
336 .filter(Badge::getVisible)
337 .map(Badge::getId)
338 .collect(Collectors.toList());
339
340 Log.d(TAG, "Uploading " + (!profileName.isEmpty() ? "non-" : "") + "empty profile name.");
341 Log.d(TAG, "Uploading " + (!Util.isEmpty(about) ? "non-" : "") + "empty about.");
342 Log.d(TAG, "Uploading " + (!Util.isEmpty(aboutEmoji) ? "non-" : "") + "empty emoji.");
343 Log.d(TAG, "Uploading " + (paymentsAddress != null ? "non-" : "") + "empty payments address.");
344 Log.d(TAG, "Uploading " + ((!badgeIds.isEmpty()) ? "non-" : "") + "empty badge list.");
345
346 if (avatar.keepTheSame) {
347 Log.d(TAG, "Leaving avatar unchanged. We think we " + (avatar.hasAvatar ? "" : "do not ") + "have one.");
348 } else {
349 Log.d(TAG, "Uploading " + (avatar.stream != null && avatar.stream.getLength() != 0 ? "non-" : "") + "empty avatar.");
350 }
351
352 ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
353 SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
354 String avatarPath = accountManager.setVersionedProfile(SignalStore.account().requireAci(),
355 profileKey,
356 profileName.serialize(),
357 about,
358 aboutEmoji,
359 Optional.ofNullable(paymentsAddress),
360 avatar,
361 badgeIds,
362 SignalStore.phoneNumberPrivacy().isPhoneNumberSharingEnabled()).orElse(null);
363 SignalStore.registrationValues().markHasUploadedProfile();
364 if (!avatar.keepTheSame) {
365 SignalDatabase.recipients().setProfileAvatar(Recipient.self().getId(), avatarPath);
366 }
367 ApplicationDependencies.getJobManager().add(new RefreshOwnProfileJob());
368 }
369
370 private static @Nullable PaymentAddress getSelfPaymentsAddressProtobuf() {
371 if (!SignalStore.paymentsValues().mobileCoinPaymentsEnabled()) {
372 return null;
373 } else {
374 IdentityKeyPair identityKeyPair = SignalStore.account().getAciIdentityKey();
375 MobileCoinPublicAddress publicAddress = ApplicationDependencies.getPayments()
376 .getWallet()
377 .getMobileCoinPublicAddress();
378
379 return MobileCoinPublicAddressProfileUtil.signPaymentsAddress(publicAddress.serialize(), identityKeyPair);
380 }
381 }
382
383 private static Optional<UnidentifiedAccess> getUnidentifiedAccess(@NonNull Context context, @NonNull Recipient recipient) {
384 Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient, false);
385
386 if (unidentifiedAccess.isPresent()) {
387 return unidentifiedAccess.get().getTargetUnidentifiedAccess();
388 }
389
390 return Optional.empty();
391 }
392
393 private static @NonNull SignalServiceAddress toSignalServiceAddress(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
394 if (recipient.getRegistered() == RecipientTable.RegisteredState.NOT_REGISTERED) {
395 if (recipient.hasServiceId()) {
396 return new SignalServiceAddress(recipient.requireServiceId(), recipient.getE164().orElse(null));
397 } else {
398 throw new IOException(recipient.getId() + " not registered!");
399 }
400 } else {
401 return RecipientUtil.toSignalServiceAddress(context, recipient);
402 }
403 }
404}