package org.thoughtcrime.securesms.jobs; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.ProfileUtil; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import java.io.IOException; import java.util.List; /** * Retrieves a users profile and sets the appropriate local fields. If fetching the profile of the * local user, use {@link RefreshOwnProfileJob} instead. */ public class RetrieveProfileJob extends BaseJob { public static final String KEY = "RetrieveProfileJob"; private static final String TAG = RetrieveProfileJob.class.getSimpleName(); private static final String KEY_RECIPIENT = "recipient"; private final Recipient recipient; public RetrieveProfileJob(@NonNull Recipient recipient) { this(new Job.Parameters.Builder() .addConstraint(NetworkConstraint.KEY) .setMaxAttempts(3) .build(), recipient); } private RetrieveProfileJob(@NonNull Job.Parameters parameters, @NonNull Recipient recipient) { super(parameters); this.recipient = recipient; } @Override public @NonNull Data serialize() { return new Data.Builder().putString(KEY_RECIPIENT, recipient.getId().serialize()).build(); } @Override public @NonNull String getFactoryKey() { return KEY; } @Override public void onRun() throws IOException { Log.i(TAG, "Retrieving profile of " + recipient.getId()); Recipient resolved = recipient.resolve(); if (resolved.isGroup()) handleGroupRecipient(resolved); else handleIndividualRecipient(resolved); } @Override public boolean onShouldRetry(@NonNull Exception e) { return false; } @Override public void onFailure() {} private void handleIndividualRecipient(Recipient recipient) throws IOException { if (recipient.hasServiceIdentifier()) handlePhoneNumberRecipient(recipient); else Log.w(TAG, "Skipping fetching profile of non-Signal recipient"); } private void handlePhoneNumberRecipient(Recipient recipient) throws IOException { ProfileAndCredential profileAndCredential = ProfileUtil.retrieveProfile(context, recipient, getRequestType(recipient)); SignalServiceProfile profile = profileAndCredential.getProfile(); ProfileKey recipientProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey()); if (recipientProfileKey == null) { Log.i(TAG, "No profile key available for " + recipient.getId()); } else { Log.i(TAG, "Profile key available for " + recipient.getId()); } setProfileName(recipient, profile.getName()); setProfileAvatar(recipient, profile.getAvatar()); if (FeatureFlags.usernames()) setUsername(recipient, profile.getUsername()); setProfileCapabilities(recipient, profile.getCapabilities()); setIdentityKey(recipient, profile.getIdentityKey()); setUnidentifiedAccessMode(recipient, profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess()); if (recipientProfileKey != null) { Optional profileKeyCredential = profileAndCredential.getProfileKeyCredential(); if (profileKeyCredential.isPresent()) { setProfileKeyCredential(recipient, recipientProfileKey, profileKeyCredential.get()); } } } private void setProfileKeyCredential(@NonNull Recipient recipient, @NonNull ProfileKey recipientProfileKey, @NonNull ProfileKeyCredential credential) { RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); recipientDatabase.setProfileKeyCredential(recipient.getId(), recipientProfileKey, credential); } private static SignalServiceProfile.RequestType getRequestType(@NonNull Recipient recipient) { return FeatureFlags.VERSIONED_PROFILES && !recipient.hasProfileKeyCredential() ? SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL : SignalServiceProfile.RequestType.PROFILE; } private void handleGroupRecipient(Recipient group) throws IOException { List recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(group.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); for (Recipient recipient : recipients) { handleIndividualRecipient(recipient); } } private void setIdentityKey(Recipient recipient, String identityKeyValue) { try { if (TextUtils.isEmpty(identityKeyValue)) { Log.w(TAG, "Identity key is missing on profile!"); return; } IdentityKey identityKey = new IdentityKey(Base64.decode(identityKeyValue), 0); if (!DatabaseFactory.getIdentityDatabase(context) .getIdentity(recipient.getId()) .isPresent()) { Log.w(TAG, "Still first use..."); return; } IdentityUtil.saveIdentity(context, recipient.requireServiceId(), identityKey); } catch (InvalidKeyException | IOException e) { Log.w(TAG, e); } } private void setUnidentifiedAccessMode(Recipient recipient, String unidentifiedAccessVerifier, boolean unrestrictedUnidentifiedAccess) { RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey()); if (unrestrictedUnidentifiedAccess && unidentifiedAccessVerifier != null) { if (recipient.getUnidentifiedAccessMode() != UnidentifiedAccessMode.UNRESTRICTED) { Log.i(TAG, "Marking recipient UD status as unrestricted."); recipientDatabase.setUnidentifiedAccessMode(recipient.getId(), UnidentifiedAccessMode.UNRESTRICTED); } } else if (profileKey == null || unidentifiedAccessVerifier == null) { if (recipient.getUnidentifiedAccessMode() != UnidentifiedAccessMode.DISABLED) { Log.i(TAG, "Marking recipient UD status as disabled."); recipientDatabase.setUnidentifiedAccessMode(recipient.getId(), UnidentifiedAccessMode.DISABLED); } } else { ProfileCipher profileCipher = new ProfileCipher(profileKey); boolean verifiedUnidentifiedAccess; try { verifiedUnidentifiedAccess = profileCipher.verifyUnidentifiedAccess(Base64.decode(unidentifiedAccessVerifier)); } catch (IOException e) { Log.w(TAG, e); verifiedUnidentifiedAccess = false; } UnidentifiedAccessMode mode = verifiedUnidentifiedAccess ? UnidentifiedAccessMode.ENABLED : UnidentifiedAccessMode.DISABLED; if (recipient.getUnidentifiedAccessMode() != mode) { Log.i(TAG, "Marking recipient UD status as " + mode.name() + " after verification."); recipientDatabase.setUnidentifiedAccessMode(recipient.getId(), mode); } } } private void setProfileName(Recipient recipient, String profileName) { try { ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey()); if (profileKey == null) return; String plaintextProfileName = ProfileUtil.decryptName(profileKey, profileName); if (!Util.equals(plaintextProfileName, recipient.getProfileName().serialize())) { Log.i(TAG, "Profile name updated. Writing new value."); DatabaseFactory.getRecipientDatabase(context).setProfileName(recipient.getId(), ProfileName.fromSerialized(plaintextProfileName)); } if (TextUtils.isEmpty(plaintextProfileName)) { Log.i(TAG, "No profile name set."); } } catch (InvalidCiphertextException | IOException e) { Log.w(TAG, e); } } private void setProfileAvatar(Recipient recipient, String profileAvatar) { if (recipient.getProfileKey() == null) return; if (!Util.equals(profileAvatar, recipient.getProfileAvatar())) { ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(recipient, profileAvatar)); } else { Log.d(TAG, "Skipping avatar fetch for " + recipient.getId()); } } private void setUsername(Recipient recipient, @Nullable String username) { DatabaseFactory.getRecipientDatabase(context).setUsername(recipient.getId(), username); } private void setProfileCapabilities(@NonNull Recipient recipient, @Nullable SignalServiceProfile.Capabilities capabilities) { if (capabilities == null) { return; } DatabaseFactory.getRecipientDatabase(context).setCapabilities(recipient.getId(), capabilities); } public static final class Factory implements Job.Factory { @Override public @NonNull RetrieveProfileJob create(@NonNull Parameters parameters, @NonNull Data data) { return new RetrieveProfileJob(parameters, Recipient.resolved(RecipientId.from(data.getString(KEY_RECIPIENT)))); } } }