]> nmode's Git Repositories - signal-cli/blobdiff - lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java
Update libsignal-service
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / ProfileHelper.java
index 4bc00317e65d6acda3c88a0bffa5df5ab94b5178..af16ed1d92b38b192f8984c6c7d41c5ea90e922c 100644 (file)
@@ -1,27 +1,30 @@
 package org.asamk.signal.manager.helper;
 
-import com.google.protobuf.InvalidProtocolBufferException;
-
-import org.asamk.signal.manager.SignalDependencies;
+import org.asamk.signal.manager.api.GroupNotFoundException;
+import org.asamk.signal.manager.api.NotAGroupMemberException;
+import org.asamk.signal.manager.api.PhoneNumberSharingMode;
+import org.asamk.signal.manager.api.Profile;
 import org.asamk.signal.manager.config.ServiceConfig;
-import org.asamk.signal.manager.groups.GroupNotFoundException;
-import org.asamk.signal.manager.groups.NotAGroupMemberException;
+import org.asamk.signal.manager.internal.SignalDependencies;
+import org.asamk.signal.manager.jobs.SyncStorageJob;
 import org.asamk.signal.manager.storage.SignalAccount;
 import org.asamk.signal.manager.storage.groups.GroupInfoV2;
-import org.asamk.signal.manager.storage.recipients.Profile;
 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
 import org.asamk.signal.manager.storage.recipients.RecipientId;
 import org.asamk.signal.manager.util.IOUtils;
 import org.asamk.signal.manager.util.KeyUtils;
+import org.asamk.signal.manager.util.PaymentUtils;
 import org.asamk.signal.manager.util.ProfileUtils;
 import org.asamk.signal.manager.util.Utils;
+import org.jetbrains.annotations.Nullable;
 import org.signal.libsignal.protocol.IdentityKey;
 import org.signal.libsignal.protocol.InvalidKeyException;
+import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
 import org.signal.libsignal.zkgroup.profiles.ProfileKey;
-import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
+import org.whispersystems.signalservice.api.NetworkResultUtil;
+import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
 import org.whispersystems.signalservice.api.profiles.AvatarUploadParams;
 import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
 import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
@@ -29,14 +32,13 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
 import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
 import org.whispersystems.signalservice.api.services.ProfileService;
-import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
+import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil;
 
-import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.file.Files;
 import java.util.Base64;
-import java.util.Date;
+import java.util.Collection;
 import java.util.List;
 import java.util.Locale;
 import java.util.Objects;
@@ -49,7 +51,7 @@ import io.reactivex.rxjava3.core.Single;
 
 public final class ProfileHelper {
 
-    private final static Logger logger = LoggerFactory.getLogger(ProfileHelper.class);
+    private static final Logger logger = LoggerFactory.getLogger(ProfileHelper.class);
 
     private final SignalAccount account;
     private final SignalDependencies dependencies;
@@ -62,11 +64,14 @@ public final class ProfileHelper {
     }
 
     public void rotateProfileKey() throws IOException {
+        // refresh our profile, before creating a new profile key
+        getSelfProfile();
         var profileKey = KeyUtils.createProfileKey();
         account.setProfileKey(profileKey);
         context.getAccountHelper().updateAccountAttributes();
-        setProfile(true, true, null, null, null, null, null);
-        // TODO update profile key in storage
+        setProfile(true, true, null, null, null, null, null, null);
+        account.getRecipientStore().rotateSelfStorageId();
+        context.getJobExecutor().enqueueJob(new SyncStorageJob());
 
         final var recipientIds = account.getRecipientStore().getRecipientIdsWithEnabledProfileSharing();
         for (final var recipientId : recipientIds) {
@@ -77,7 +82,7 @@ public final class ProfileHelper {
         final var activeGroupIds = account.getGroupStore()
                 .getGroups()
                 .stream()
-                .filter(g -> g instanceof GroupInfoV2 && g.isMember(selfRecipientId))
+                .filter(g -> g instanceof GroupInfoV2 && g.isMember(selfRecipientId) && g.isProfileSharingEnabled())
                 .map(g -> (GroupInfoV2) g)
                 .map(GroupInfoV2::getGroupId)
                 .toList();
@@ -94,28 +99,32 @@ public final class ProfileHelper {
         return getRecipientProfile(recipientId, false);
     }
 
+    public List<Profile> getRecipientProfiles(Collection<RecipientId> recipientIds) {
+        return getRecipientProfiles(recipientIds, false);
+    }
+
     public void refreshRecipientProfile(RecipientId recipientId) {
         getRecipientProfile(recipientId, true);
     }
 
-    public List<ProfileKeyCredential> getRecipientProfileKeyCredential(List<RecipientId> recipientIds) {
-        try {
-            account.getRecipientStore().setBulkUpdating(true);
-            final var profileFetches = Flowable.fromIterable(recipientIds)
-                    .filter(recipientId -> account.getProfileStore().getProfileKeyCredential(recipientId) == null)
-                    .map(recipientId -> retrieveProfile(recipientId,
-                            SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL).onErrorComplete());
-            Maybe.merge(profileFetches, 10).blockingSubscribe();
-        } finally {
-            account.getRecipientStore().setBulkUpdating(false);
-        }
+    public void refreshRecipientProfiles(Collection<RecipientId> recipientIds) {
+        getRecipientProfiles(recipientIds, true);
+    }
+
+    public List<ExpiringProfileKeyCredential> getExpiringProfileKeyCredential(List<RecipientId> recipientIds) {
+        final var profileFetches = Flowable.fromIterable(recipientIds)
+                .filter(recipientId -> !ExpiringProfileCredentialUtil.isValid(account.getProfileStore()
+                        .getExpiringProfileKeyCredential(recipientId)))
+                .map(recipientId -> retrieveProfile(recipientId,
+                        SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL).onErrorComplete());
+        Maybe.merge(profileFetches, 10).blockingSubscribe();
 
-        return recipientIds.stream().map(r -> account.getProfileStore().getProfileKeyCredential(r)).toList();
+        return recipientIds.stream().map(r -> account.getProfileStore().getExpiringProfileKeyCredential(r)).toList();
     }
 
-    public ProfileKeyCredential getRecipientProfileKeyCredential(RecipientId recipientId) {
-        var profileKeyCredential = account.getProfileStore().getProfileKeyCredential(recipientId);
-        if (profileKeyCredential != null) {
+    public ExpiringProfileKeyCredential getExpiringProfileKeyCredential(RecipientId recipientId) {
+        var profileKeyCredential = account.getProfileStore().getExpiringProfileKeyCredential(recipientId);
+        if (ExpiringProfileCredentialUtil.isValid(profileKeyCredential)) {
             return profileKeyCredential;
         }
 
@@ -126,7 +135,7 @@ public final class ProfileHelper {
             return null;
         }
 
-        return account.getProfileStore().getProfileKeyCredential(recipientId);
+        return account.getProfileStore().getExpiringProfileKeyCredential(recipientId);
     }
 
     /**
@@ -137,9 +146,14 @@ public final class ProfileHelper {
      * @param avatar     if avatar is null the image from the local avatar store is used (if present),
      */
     public void setProfile(
-            String givenName, final String familyName, String about, String aboutEmoji, Optional<File> avatar
+            String givenName,
+            final String familyName,
+            String about,
+            String aboutEmoji,
+            Optional<String> avatar,
+            byte[] mobileCoinAddress
     ) throws IOException {
-        setProfile(true, false, givenName, familyName, about, aboutEmoji, avatar);
+        setProfile(true, false, givenName, familyName, about, aboutEmoji, avatar, mobileCoinAddress);
     }
 
     public void setProfile(
@@ -149,7 +163,8 @@ public final class ProfileHelper {
             final String familyName,
             String about,
             String aboutEmoji,
-            Optional<File> avatar
+            Optional<String> avatar,
+            byte[] mobileCoinAddress
     ) throws IOException {
         var profile = getSelfProfile();
         var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
@@ -165,26 +180,27 @@ public final class ProfileHelper {
         if (aboutEmoji != null) {
             builder.withAboutEmoji(aboutEmoji);
         }
+        if (mobileCoinAddress != null) {
+            builder.withMobileCoinAddress(mobileCoinAddress);
+        }
         var newProfile = builder.build();
 
         if (uploadProfile) {
             final var streamDetails = avatar != null && avatar.isPresent()
-                    ? Utils.createStreamDetailsFromFile(avatar.get())
+                    ? Utils.createStreamDetails(avatar.get())
+                    .first()
                     : forceUploadAvatar && avatar == null ? context.getAvatarStore()
                             .retrieveProfileAvatar(account.getSelfRecipientAddress()) : null;
             try (streamDetails) {
                 final var avatarUploadParams = streamDetails != null
                         ? AvatarUploadParams.forAvatar(streamDetails)
                         : avatar == null ? AvatarUploadParams.unchanged(true) : AvatarUploadParams.unchanged(false);
-                final var paymentsAddress = Optional.ofNullable(newProfile.getPaymentAddress()).map(data -> {
-                    try {
-                        return SignalServiceProtos.PaymentAddress.parseFrom(data);
-                    } catch (InvalidProtocolBufferException e) {
-                        return null;
-                    }
-                });
+                final var paymentsAddress = Optional.ofNullable(newProfile.getMobileCoinAddress())
+                        .map(address -> PaymentUtils.signPaymentsAddress(address,
+                                account.getAciIdentityKeyPair().getPrivateKey()))
+                        .orElse(null);
                 logger.debug("Uploading new profile");
-                final var avatarPath = dependencies.getAccountManager()
+                final var avatarPath = NetworkResultUtil.toSetProfileLegacy(dependencies.getProfileApi()
                         .setVersionedProfile(account.getAci(),
                                 account.getProfileKey(),
                                 newProfile.getInternalServiceName(),
@@ -192,9 +208,11 @@ public final class ProfileHelper {
                                 newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(),
                                 paymentsAddress,
                                 avatarUploadParams,
-                                List.of(/* TODO implement support for badges */));
+                                List.of(/* TODO implement support for badges */),
+                                account.getConfigurationStore().getPhoneNumberSharingMode()
+                                        == PhoneNumberSharingMode.EVERYBODY));
                 if (!avatarUploadParams.keepTheSame) {
-                    builder.withAvatarUrlPath(avatarPath.orElse(null));
+                    builder.withAvatarUrlPath(avatarPath);
                 }
                 newProfile = builder.build();
             }
@@ -202,9 +220,11 @@ public final class ProfileHelper {
 
         if (avatar != null) {
             if (avatar.isPresent()) {
-                context.getAvatarStore()
-                        .storeProfileAvatar(account.getSelfRecipientAddress(),
-                                outputStream -> IOUtils.copyFileToStream(avatar.get(), outputStream));
+                try (final var streamDetails = Utils.createStreamDetails(avatar.get()).first()) {
+                    context.getAvatarStore()
+                            .storeProfileAvatar(account.getSelfRecipientAddress(),
+                                    outputStream -> IOUtils.copyStream(streamDetails.getStream(), outputStream));
+                }
             } else {
                 context.getAvatarStore().deleteProfileAvatar(account.getSelfRecipientAddress());
             }
@@ -216,19 +236,15 @@ public final class ProfileHelper {
         return getRecipientProfile(account.getSelfRecipientId());
     }
 
-    public List<Profile> getRecipientProfile(List<RecipientId> recipientIds) {
-        try {
-            account.getRecipientStore().setBulkUpdating(true);
-            final var profileFetches = Flowable.fromIterable(recipientIds)
-                    .filter(recipientId -> isProfileRefreshRequired(account.getProfileStore().getProfile(recipientId)))
-                    .map(recipientId -> retrieveProfile(recipientId,
-                            SignalServiceProfile.RequestType.PROFILE).onErrorComplete());
-            Maybe.merge(profileFetches, 10).blockingSubscribe();
-        } finally {
-            account.getRecipientStore().setBulkUpdating(false);
-        }
+    private List<Profile> getRecipientProfiles(Collection<RecipientId> recipientIds, boolean force) {
+        final var profileStore = account.getProfileStore();
+        final var profileFetches = Flowable.fromIterable(recipientIds)
+                .filter(recipientId -> force || isProfileRefreshRequired(profileStore.getProfile(recipientId)))
+                .map(recipientId -> retrieveProfile(recipientId,
+                        SignalServiceProfile.RequestType.PROFILE).onErrorComplete());
+        Maybe.merge(profileFetches, 10).blockingSubscribe();
 
-        return recipientIds.stream().map(r -> account.getProfileStore().getProfile(r)).toList();
+        return recipientIds.stream().map(profileStore::getProfile).toList();
     }
 
     private Profile getRecipientProfile(RecipientId recipientId, boolean force) {
@@ -256,13 +272,10 @@ public final class ProfileHelper {
         return now - profile.getLastUpdateTimestamp() >= 6 * 60 * 60 * 1000;
     }
 
-    private SignalServiceProfile retrieveProfileSync(String username) throws IOException {
-        final var locale = Utils.getDefaultLocale(Locale.US);
-        return dependencies.getMessageReceiver().retrieveProfileByUsername(username, Optional.empty(), locale);
-    }
-
     private Profile decryptProfileAndDownloadAvatar(
-            final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
+            final RecipientId recipientId,
+            final ProfileKey profileKey,
+            final SignalServiceProfile encryptedProfile
     ) {
         final var avatarPath = encryptedProfile.getAvatar();
         downloadProfileAvatar(recipientId, avatarPath, profileKey);
@@ -271,12 +284,14 @@ public final class ProfileHelper {
     }
 
     public void downloadProfileAvatar(
-            final RecipientId recipientId, final String avatarPath, final ProfileKey profileKey
+            final RecipientId recipientId,
+            final String avatarPath,
+            final ProfileKey profileKey
     ) {
         var profile = account.getProfileStore().getProfile(recipientId);
         if (profile == null || !Objects.equals(avatarPath, profile.getAvatarUrlPath())) {
             logger.trace("Downloading profile avatar for {}", recipientId);
-            downloadProfileAvatar(account.getRecipientStore().resolveRecipientAddress(recipientId),
+            downloadProfileAvatar(account.getRecipientAddressResolver().resolveRecipientAddress(recipientId),
                     avatarPath,
                     profileKey);
             var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
@@ -299,7 +314,8 @@ public final class ProfileHelper {
     }
 
     private Single<ProfileAndCredential> retrieveProfile(
-            RecipientId recipientId, SignalServiceProfile.RequestType requestType
+            RecipientId recipientId,
+            SignalServiceProfile.RequestType requestType
     ) {
         var unidentifiedAccess = getUnidentifiedAccess(recipientId);
         var profileKey = Optional.ofNullable(account.getProfileStore().getProfileKey(recipientId));
@@ -313,10 +329,11 @@ public final class ProfileHelper {
             final var encryptedProfile = p.getProfile();
 
             if (requestType == SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL
-                    || account.getProfileStore().getProfileKeyCredential(recipientId) == null) {
+                    || !ExpiringProfileCredentialUtil.isValid(account.getProfileStore()
+                    .getExpiringProfileKeyCredential(recipientId))) {
                 logger.trace("Storing profile credential");
-                final var profileKeyCredential = p.getProfileKeyCredential().orElse(null);
-                account.getProfileStore().storeProfileKeyCredential(recipientId, profileKeyCredential);
+                final var profileKeyCredential = p.getExpiringProfileKeyCredential().orElse(null);
+                account.getProfileStore().storeExpiringProfileKeyCredential(recipientId, profileKeyCredential);
             }
 
             final var profile = account.getProfileStore().getProfile(recipientId);
@@ -336,10 +353,22 @@ public final class ProfileHelper {
                         .build();
             }
 
+            if (recipientId.equals(account.getSelfRecipientId())) {
+                final var isUnrestricted = encryptedProfile.isUnrestrictedUnidentifiedAccess();
+                if (account.isUnrestrictedUnidentifiedAccess() != isUnrestricted) {
+                    account.setUnrestrictedUnidentifiedAccess(isUnrestricted);
+                }
+                if (account.isPrimaryDevice() && profile != null && newProfile.getCapabilities()
+                        .contains(Profile.Capability.storageServiceEncryptionV2Capability) && !profile.getCapabilities()
+                        .contains(Profile.Capability.storageServiceEncryptionV2Capability)) {
+                    context.getJobExecutor().enqueueJob(new SyncStorageJob(true));
+                }
+            }
+
             try {
                 logger.trace("Storing identity");
                 final var identityKey = new IdentityKey(Base64.getDecoder().decode(encryptedProfile.getIdentityKey()));
-                account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date());
+                account.getIdentityKeyStore().saveIdentity(p.getProfile().getServiceId(), identityKey);
             } catch (InvalidKeyException ignored) {
                 logger.warn("Got invalid identity key in profile for {}",
                         context.getRecipientHelper().resolveSignalServiceAddress(recipientId).getIdentifier());
@@ -347,6 +376,7 @@ public final class ProfileHelper {
 
             logger.trace("Storing profile");
             account.getProfileStore().storeProfile(recipientId, newProfile);
+            account.getRecipientStore().markRegistered(recipientId, true);
 
             logger.trace("Done handling retrieved profile");
         }).doOnError(e -> {
@@ -358,6 +388,10 @@ public final class ProfileHelper {
                     .withUnidentifiedAccessMode(Profile.UnidentifiedAccessMode.UNKNOWN)
                     .withCapabilities(Set.of())
                     .build();
+            if (e instanceof NotFoundException) {
+                logger.debug("Marking recipient {} as unregistered after 404 profile fetch.", recipientId);
+                account.getRecipientStore().markRegistered(recipientId, false);
+            }
 
             account.getProfileStore().storeProfile(recipientId, newProfile);
         });
@@ -366,7 +400,7 @@ public final class ProfileHelper {
     private Single<ProfileAndCredential> retrieveProfile(
             SignalServiceAddress address,
             Optional<ProfileKey> profileKey,
-            Optional<UnidentifiedAccess> unidentifiedAccess,
+            @Nullable SealedSenderAccess unidentifiedAccess,
             SignalServiceProfile.RequestType requestType
     ) {
         final var profileService = dependencies.getProfileService();
@@ -386,9 +420,7 @@ public final class ProfileHelper {
         });
     }
 
-    private void downloadProfileAvatar(
-            RecipientAddress address, String avatarPath, ProfileKey profileKey
-    ) {
+    private void downloadProfileAvatar(RecipientAddress address, String avatarPath, ProfileKey profileKey) {
         if (avatarPath == null) {
             try {
                 context.getAvatarStore().deleteProfileAvatar(address);
@@ -408,7 +440,9 @@ public final class ProfileHelper {
     }
 
     private void retrieveProfileAvatar(
-            String avatarPath, ProfileKey profileKey, OutputStream outputStream
+            String avatarPath,
+            ProfileKey profileKey,
+            OutputStream outputStream
     ) throws IOException {
         var tmpFile = IOUtils.createTempFile();
         try (var input = dependencies.getMessageReceiver()
@@ -429,13 +463,7 @@ public final class ProfileHelper {
         }
     }
 
-    private Optional<UnidentifiedAccess> getUnidentifiedAccess(RecipientId recipientId) {
-        var unidentifiedAccess = context.getUnidentifiedAccessHelper().getAccessFor(recipientId, true);
-
-        if (unidentifiedAccess.isPresent()) {
-            return unidentifiedAccess.get().getTargetUnidentifiedAccess();
-        }
-
-        return Optional.empty();
+    private @Nullable SealedSenderAccess getUnidentifiedAccess(RecipientId recipientId) {
+        return context.getUnidentifiedAccessHelper().getSealedSenderAccessFor(recipientId, true);
     }
 }