That fuck shit the fascists are using
at master 404 lines 21 kB view raw
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}