]> nmode's Git Repositories - signal-cli/commitdiff
Move more profile functionality to ProfileHelper
authorAsamK <asamk@gmx.de>
Thu, 26 Aug 2021 08:56:30 +0000 (10:56 +0200)
committerAsamK <asamk@gmx.de>
Thu, 26 Aug 2021 08:56:30 +0000 (10:56 +0200)
lib/src/main/java/org/asamk/signal/manager/HandleAction.java
lib/src/main/java/org/asamk/signal/manager/Manager.java
lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java

index 2396df06bfb4b6d323cf29992507ce4bbc85041f..8639806fd8790463bec59b29ead55329a152aae3 100644 (file)
@@ -170,7 +170,7 @@ class RetrieveProfileAction implements HandleAction {
 
     @Override
     public void execute(Manager m) throws Throwable {
 
     @Override
     public void execute(Manager m) throws Throwable {
-        m.getRecipientProfile(recipientId, true);
+        m.refreshRecipientProfile(recipientId);
     }
 
     @Override
     }
 
     @Override
index 5de65f3e86e0a0813de7a90c3c3dee0047999a78..ac7b571f24aca8f9ade6b25bc8f860398e270227 100644 (file)
@@ -58,14 +58,12 @@ import org.asamk.signal.manager.storage.stickers.StickerPackId;
 import org.asamk.signal.manager.util.AttachmentUtils;
 import org.asamk.signal.manager.util.IOUtils;
 import org.asamk.signal.manager.util.KeyUtils;
 import org.asamk.signal.manager.util.AttachmentUtils;
 import org.asamk.signal.manager.util.IOUtils;
 import org.asamk.signal.manager.util.KeyUtils;
-import org.asamk.signal.manager.util.ProfileUtils;
 import org.asamk.signal.manager.util.StickerUtils;
 import org.asamk.signal.manager.util.Utils;
 import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
 import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
 import org.signal.zkgroup.InvalidInputException;
 import org.signal.zkgroup.profiles.ProfileKey;
 import org.asamk.signal.manager.util.StickerUtils;
 import org.asamk.signal.manager.util.Utils;
 import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
 import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
 import org.signal.zkgroup.InvalidInputException;
 import org.signal.zkgroup.profiles.ProfileKey;
-import org.signal.zkgroup.profiles.ProfileKeyCredential;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.whispersystems.libsignal.IdentityKey;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.whispersystems.libsignal.IdentityKey;
@@ -106,8 +104,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
 import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
 import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
 import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
 import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
-import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
-import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
 import org.whispersystems.signalservice.api.util.DeviceNameUtil;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
 import org.whispersystems.signalservice.api.util.DeviceNameUtil;
@@ -137,7 +133,6 @@ import java.nio.file.Files;
 import java.security.SignatureException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.security.SignatureException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Base64;
 import java.util.Collection;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Collection;
 import java.util.Date;
 import java.util.HashMap;
@@ -205,26 +200,29 @@ public class Manager implements Closeable {
                 account.getSignalProtocolStore(),
                 executor,
                 sessionLock);
                 account.getSignalProtocolStore(),
                 executor,
                 sessionLock);
-        this.pinHelper = new PinHelper(dependencies.getKeyBackupService());
+        this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
+        this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath());
+        this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath());
 
 
+        this.pinHelper = new PinHelper(dependencies.getKeyBackupService());
         final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey,
                 account.getProfileStore()::getProfileKey,
                 this::getRecipientProfile,
                 this::getSenderCertificate);
         final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey,
                 account.getProfileStore()::getProfileKey,
                 this::getRecipientProfile,
                 this::getSenderCertificate);
-        this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey,
+        this.profileHelper = new ProfileHelper(account,
+                dependencies,
+                avatarStore,
+                account.getProfileStore()::getProfileKey,
                 unidentifiedAccessHelper::getAccessFor,
                 dependencies::getProfileService,
                 dependencies::getMessageReceiver,
                 this::resolveSignalServiceAddress);
                 unidentifiedAccessHelper::getAccessFor,
                 dependencies::getProfileService,
                 dependencies::getMessageReceiver,
                 this::resolveSignalServiceAddress);
-        final GroupV2Helper groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential,
+        final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential,
                 this::getRecipientProfile,
                 account::getSelfRecipientId,
                 dependencies.getGroupsV2Operations(),
                 dependencies.getGroupsV2Api(),
                 this::resolveSignalServiceAddress);
                 this::getRecipientProfile,
                 account::getSelfRecipientId,
                 dependencies.getGroupsV2Operations(),
                 dependencies.getGroupsV2Api(),
                 this::resolveSignalServiceAddress);
-        this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
-        this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath());
-        this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath());
         this.sendHelper = new SendHelper(account,
                 dependencies,
                 unidentifiedAccessHelper,
         this.sendHelper = new SendHelper(account,
                 dependencies,
                 unidentifiedAccessHelper,
@@ -246,7 +244,7 @@ public class Manager implements Closeable {
         return account.getUsername();
     }
 
         return account.getUsername();
     }
 
-    public SignalServiceAddress getSelfAddress() {
+    private SignalServiceAddress getSelfAddress() {
         return account.getSelfAddress();
     }
 
         return account.getSelfAddress();
     }
 
@@ -377,45 +375,12 @@ public class Manager implements Closeable {
     public void setProfile(
             String givenName, final String familyName, String about, String aboutEmoji, Optional<File> avatar
     ) throws IOException {
     public void setProfile(
             String givenName, final String familyName, String about, String aboutEmoji, Optional<File> avatar
     ) throws IOException {
-        var profile = getRecipientProfile(account.getSelfRecipientId());
-        var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
-        if (givenName != null) {
-            builder.withGivenName(givenName);
-        }
-        if (familyName != null) {
-            builder.withFamilyName(familyName);
-        }
-        if (about != null) {
-            builder.withAbout(about);
-        }
-        if (aboutEmoji != null) {
-            builder.withAboutEmoji(aboutEmoji);
-        }
-        var newProfile = builder.build();
-
-        try (final var streamDetails = avatar == null
-                ? avatarStore.retrieveProfileAvatar(getSelfAddress())
-                : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) {
-            dependencies.getAccountManager()
-                    .setVersionedProfile(account.getUuid(),
-                            account.getProfileKey(),
-                            newProfile.getInternalServiceName(),
-                            newProfile.getAbout() == null ? "" : newProfile.getAbout(),
-                            newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(),
-                            Optional.absent(),
-                            streamDetails);
-        }
+        profileHelper.setProfile(givenName, familyName, about, aboutEmoji, avatar);
 
 
-        if (avatar != null) {
-            if (avatar.isPresent()) {
-                avatarStore.storeProfileAvatar(getSelfAddress(),
-                        outputStream -> IOUtils.copyFileToStream(avatar.get(), outputStream));
-            } else {
-                avatarStore.deleteProfileAvatar(getSelfAddress());
-            }
-        }
-        account.getProfileStore().storeProfile(account.getSelfRecipientId(), newProfile);
+        sendSyncFetchProfileMessage();
+    }
 
 
+    private void sendSyncFetchProfileMessage() throws IOException {
         sendHelper.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
     }
 
         sendHelper.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
     }
 
@@ -522,134 +487,12 @@ public class Manager implements Closeable {
         return record;
     }
 
         return record;
     }
 
-    public Profile getRecipientProfile(
-            RecipientId recipientId
-    ) {
-        return getRecipientProfile(recipientId, false);
-    }
-
-    private final Set<RecipientId> pendingProfileRequest = new HashSet<>();
-
-    Profile getRecipientProfile(
-            RecipientId recipientId, boolean force
-    ) {
-        var profile = account.getProfileStore().getProfile(recipientId);
-
-        var now = System.currentTimeMillis();
-        // Profiles are cached for 24h before retrieving them again, unless forced
-        if (!force && profile != null && now - profile.getLastUpdateTimestamp() < 24 * 60 * 60 * 1000) {
-            return profile;
-        }
-
-        synchronized (pendingProfileRequest) {
-            if (pendingProfileRequest.contains(recipientId)) {
-                return profile;
-            }
-            pendingProfileRequest.add(recipientId);
-        }
-        final SignalServiceProfile encryptedProfile;
-        try {
-            encryptedProfile = retrieveEncryptedProfile(recipientId);
-        } finally {
-            synchronized (pendingProfileRequest) {
-                pendingProfileRequest.remove(recipientId);
-            }
-        }
-        if (encryptedProfile == null) {
-            return null;
-        }
-
-        profile = decryptProfileIfKeyKnown(recipientId, encryptedProfile);
-        account.getProfileStore().storeProfile(recipientId, profile);
-
-        return profile;
-    }
-
-    private Profile decryptProfileIfKeyKnown(
-            final RecipientId recipientId, final SignalServiceProfile encryptedProfile
-    ) {
-        var profileKey = account.getProfileStore().getProfileKey(recipientId);
-        if (profileKey == null) {
-            return new Profile(System.currentTimeMillis(),
-                    null,
-                    null,
-                    null,
-                    null,
-                    ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null),
-                    ProfileUtils.getCapabilities(encryptedProfile));
-        }
-
-        return decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile);
-    }
-
-    private SignalServiceProfile retrieveEncryptedProfile(RecipientId recipientId) {
-        try {
-            return retrieveProfileAndCredential(recipientId, SignalServiceProfile.RequestType.PROFILE).getProfile();
-        } catch (IOException e) {
-            logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
-            return null;
-        }
-    }
-
-    private ProfileAndCredential retrieveProfileAndCredential(
-            final RecipientId recipientId, final SignalServiceProfile.RequestType requestType
-    ) throws IOException {
-        final var profileAndCredential = profileHelper.retrieveProfileSync(recipientId, requestType);
-        final var profile = profileAndCredential.getProfile();
-
-        try {
-            var newIdentity = account.getIdentityKeyStore()
-                    .saveIdentity(recipientId,
-                            new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())),
-                            new Date());
-
-            if (newIdentity) {
-                account.getSessionStore().archiveSessions(recipientId);
-            }
-        } catch (InvalidKeyException ignored) {
-            logger.warn("Got invalid identity key in profile for {}",
-                    resolveSignalServiceAddress(recipientId).getIdentifier());
-        }
-        return profileAndCredential;
+    public Profile getRecipientProfile(RecipientId recipientId) {
+        return profileHelper.getRecipientProfile(recipientId);
     }
 
     }
 
-    private ProfileKeyCredential getRecipientProfileKeyCredential(RecipientId recipientId) {
-        var profileKeyCredential = account.getProfileStore().getProfileKeyCredential(recipientId);
-        if (profileKeyCredential != null) {
-            return profileKeyCredential;
-        }
-
-        ProfileAndCredential profileAndCredential;
-        try {
-            profileAndCredential = retrieveProfileAndCredential(recipientId,
-                    SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL);
-        } catch (IOException e) {
-            logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage());
-            return null;
-        }
-
-        profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull();
-        account.getProfileStore().storeProfileKeyCredential(recipientId, profileKeyCredential);
-
-        var profileKey = account.getProfileStore().getProfileKey(recipientId);
-        if (profileKey != null) {
-            final var profile = decryptProfileAndDownloadAvatar(recipientId,
-                    profileKey,
-                    profileAndCredential.getProfile());
-            account.getProfileStore().storeProfile(recipientId, profile);
-        }
-
-        return profileKeyCredential;
-    }
-
-    private Profile decryptProfileAndDownloadAvatar(
-            final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
-    ) {
-        if (encryptedProfile.getAvatar() != null) {
-            downloadProfileAvatar(resolveSignalServiceAddress(recipientId), encryptedProfile.getAvatar(), profileKey);
-        }
-
-        return ProfileUtils.decryptProfile(profileKey, encryptedProfile);
+    public void refreshRecipientProfile(RecipientId recipientId) {
+        profileHelper.refreshRecipientProfile(recipientId);
     }
 
     private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(SignalServiceAddress address) throws IOException {
     }
 
     private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(SignalServiceAddress address) throws IOException {
@@ -1784,7 +1627,7 @@ public class Manager implements Closeable {
                 if (syncMessage.getFetchType().isPresent()) {
                     switch (syncMessage.getFetchType().get()) {
                         case LOCAL_PROFILE:
                 if (syncMessage.getFetchType().isPresent()) {
                     switch (syncMessage.getFetchType().get()) {
                         case LOCAL_PROFILE:
-                            getRecipientProfile(account.getSelfRecipientId(), true);
+                            actions.add(new RetrieveProfileAction(account.getSelfRecipientId()));
                         case STORAGE_MANIFEST:
                             // TODO
                     }
                         case STORAGE_MANIFEST:
                             // TODO
                     }
@@ -1820,20 +1663,6 @@ public class Manager implements Closeable {
         }
     }
 
         }
     }
 
-    private void downloadProfileAvatar(
-            SignalServiceAddress address, String avatarPath, ProfileKey profileKey
-    ) {
-        try {
-            avatarStore.storeProfileAvatar(address,
-                    outputStream -> retrieveProfileAvatar(avatarPath, profileKey, outputStream));
-        } catch (Throwable e) {
-            if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) {
-                Thread.currentThread().interrupt();
-            }
-            logger.warn("Failed to download profile avatar, ignoring: {}", e.getMessage());
-        }
-    }
-
     public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) {
         return attachmentStore.getAttachmentFile(attachmentId);
     }
     public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) {
         return attachmentStore.getAttachmentFile(attachmentId);
     }
@@ -1862,28 +1691,6 @@ public class Manager implements Closeable {
         }
     }
 
         }
     }
 
-    private void retrieveProfileAvatar(
-            String avatarPath, ProfileKey profileKey, OutputStream outputStream
-    ) throws IOException {
-        var tmpFile = IOUtils.createTempFile();
-        try (var input = dependencies.getMessageReceiver()
-                .retrieveProfileAvatar(avatarPath,
-                        tmpFile,
-                        profileKey,
-                        ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
-            // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
-            IOUtils.copyStream(input, outputStream, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE);
-        } finally {
-            try {
-                Files.delete(tmpFile.toPath());
-            } catch (IOException e) {
-                logger.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}",
-                        tmpFile,
-                        e.getMessage());
-            }
-        }
-    }
-
     private void retrieveAttachment(
             final SignalServiceAttachment attachment, final OutputStream outputStream
     ) throws IOException {
     private void retrieveAttachment(
             final SignalServiceAttachment attachment, final OutputStream outputStream
     ) throws IOException {
@@ -2069,17 +1876,15 @@ public class Manager implements Closeable {
 
     public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) {
         final var recipientId = resolveRecipient(recipientIdentifier);
 
     public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) {
         final var recipientId = resolveRecipient(recipientIdentifier);
-        final var recipient = account.getRecipientStore().getRecipient(recipientId);
-        if (recipient == null) {
-            return null;
-        }
 
 
-        if (recipient.getContact() != null && !Util.isEmpty(recipient.getContact().getName())) {
-            return recipient.getContact().getName();
+        final var contact = account.getRecipientStore().getContact(recipientId);
+        if (contact != null && !Util.isEmpty(contact.getName())) {
+            return contact.getName();
         }
 
         }
 
-        if (recipient.getProfile() != null && recipient.getProfile() != null) {
-            return recipient.getProfile().getDisplayName();
+        final var profile = getRecipientProfile(recipientId);
+        if (profile != null) {
+            return profile.getDisplayName();
         }
 
         return null;
         }
 
         return null;
@@ -2188,7 +1993,7 @@ public class Manager implements Closeable {
             }
         } else {
             // Retrieve profile to get the current identity key from the server
             }
         } else {
             // Retrieve profile to get the current identity key from the server
-            retrieveEncryptedProfile(recipientId);
+            refreshRecipientProfile(recipientId);
         }
     }
 
         }
     }
 
index c3c74b0b8df7f5027466899da6dd6686fe185315..ac75a5730d5272f31b5653d0bbb66cca9af2f83e 100644 (file)
@@ -1,7 +1,20 @@
 package org.asamk.signal.manager.helper;
 
 package org.asamk.signal.manager.helper;
 
+import org.asamk.signal.manager.AvatarStore;
+import org.asamk.signal.manager.SignalDependencies;
+import org.asamk.signal.manager.config.ServiceConfig;
+import org.asamk.signal.manager.storage.SignalAccount;
+import org.asamk.signal.manager.storage.recipients.Profile;
 import org.asamk.signal.manager.storage.recipients.RecipientId;
 import org.asamk.signal.manager.storage.recipients.RecipientId;
+import org.asamk.signal.manager.util.IOUtils;
+import org.asamk.signal.manager.util.ProfileUtils;
+import org.asamk.signal.manager.util.Utils;
 import org.signal.zkgroup.profiles.ProfileKey;
 import org.signal.zkgroup.profiles.ProfileKey;
+import org.signal.zkgroup.profiles.ProfileKeyCredential;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.whispersystems.libsignal.IdentityKey;
+import org.whispersystems.libsignal.InvalidKeyException;
 import org.whispersystems.libsignal.util.guava.Optional;
 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
 import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
 import org.whispersystems.libsignal.util.guava.Optional;
 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
 import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
@@ -12,29 +25,43 @@ import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
 import org.whispersystems.signalservice.api.services.ProfileService;
 import org.whispersystems.signalservice.internal.ServiceResponse;
 
 import org.whispersystems.signalservice.api.services.ProfileService;
 import org.whispersystems.signalservice.internal.ServiceResponse;
 
+import java.io.File;
 import java.io.IOException;
 import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.util.Base64;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
 
 import io.reactivex.rxjava3.core.Single;
 
 public final class ProfileHelper {
 
 
 import io.reactivex.rxjava3.core.Single;
 
 public final class ProfileHelper {
 
-    private final ProfileKeyProvider profileKeyProvider;
+    private final static Logger logger = LoggerFactory.getLogger(ProfileHelper.class);
 
 
+    private final SignalAccount account;
+    private final SignalDependencies dependencies;
+    private final AvatarStore avatarStore;
+    private final ProfileKeyProvider profileKeyProvider;
     private final UnidentifiedAccessProvider unidentifiedAccessProvider;
     private final UnidentifiedAccessProvider unidentifiedAccessProvider;
-
     private final ProfileServiceProvider profileServiceProvider;
     private final ProfileServiceProvider profileServiceProvider;
-
     private final MessageReceiverProvider messageReceiverProvider;
     private final MessageReceiverProvider messageReceiverProvider;
-
     private final SignalServiceAddressResolver addressResolver;
 
     public ProfileHelper(
     private final SignalServiceAddressResolver addressResolver;
 
     public ProfileHelper(
+            final SignalAccount account,
+            final SignalDependencies dependencies,
+            final AvatarStore avatarStore,
             final ProfileKeyProvider profileKeyProvider,
             final UnidentifiedAccessProvider unidentifiedAccessProvider,
             final ProfileServiceProvider profileServiceProvider,
             final MessageReceiverProvider messageReceiverProvider,
             final SignalServiceAddressResolver addressResolver
     ) {
             final ProfileKeyProvider profileKeyProvider,
             final UnidentifiedAccessProvider unidentifiedAccessProvider,
             final ProfileServiceProvider profileServiceProvider,
             final MessageReceiverProvider messageReceiverProvider,
             final SignalServiceAddressResolver addressResolver
     ) {
+        this.account = account;
+        this.dependencies = dependencies;
+        this.avatarStore = avatarStore;
         this.profileKeyProvider = profileKeyProvider;
         this.unidentifiedAccessProvider = unidentifiedAccessProvider;
         this.profileServiceProvider = profileServiceProvider;
         this.profileKeyProvider = profileKeyProvider;
         this.unidentifiedAccessProvider = unidentifiedAccessProvider;
         this.profileServiceProvider = profileServiceProvider;
@@ -42,7 +69,193 @@ public final class ProfileHelper {
         this.addressResolver = addressResolver;
     }
 
         this.addressResolver = addressResolver;
     }
 
-    public ProfileAndCredential retrieveProfileSync(
+    public Profile getRecipientProfile(RecipientId recipientId) {
+        return getRecipientProfile(recipientId, false);
+    }
+
+    public void refreshRecipientProfile(RecipientId recipientId) {
+        getRecipientProfile(recipientId, true);
+    }
+
+    public ProfileKeyCredential getRecipientProfileKeyCredential(RecipientId recipientId) {
+        var profileKeyCredential = account.getProfileStore().getProfileKeyCredential(recipientId);
+        if (profileKeyCredential != null) {
+            return profileKeyCredential;
+        }
+
+        ProfileAndCredential profileAndCredential;
+        try {
+            profileAndCredential = retrieveProfileAndCredential(recipientId,
+                    SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL);
+        } catch (IOException e) {
+            logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage());
+            return null;
+        }
+
+        profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull();
+        account.getProfileStore().storeProfileKeyCredential(recipientId, profileKeyCredential);
+
+        var profileKey = account.getProfileStore().getProfileKey(recipientId);
+        if (profileKey != null) {
+            final var profile = decryptProfileAndDownloadAvatar(recipientId,
+                    profileKey,
+                    profileAndCredential.getProfile());
+            account.getProfileStore().storeProfile(recipientId, profile);
+        }
+
+        return profileKeyCredential;
+    }
+
+    /**
+     * @param givenName  if null, the previous givenName will be kept
+     * @param familyName if null, the previous familyName will be kept
+     * @param about      if null, the previous about text will be kept
+     * @param aboutEmoji if null, the previous about emoji will be kept
+     * @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
+    ) throws IOException {
+        var profile = getRecipientProfile(account.getSelfRecipientId());
+        var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
+        if (givenName != null) {
+            builder.withGivenName(givenName);
+        }
+        if (familyName != null) {
+            builder.withFamilyName(familyName);
+        }
+        if (about != null) {
+            builder.withAbout(about);
+        }
+        if (aboutEmoji != null) {
+            builder.withAboutEmoji(aboutEmoji);
+        }
+        var newProfile = builder.build();
+
+        try (final var streamDetails = avatar == null
+                ? avatarStore.retrieveProfileAvatar(account.getSelfAddress())
+                : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) {
+            dependencies.getAccountManager()
+                    .setVersionedProfile(account.getUuid(),
+                            account.getProfileKey(),
+                            newProfile.getInternalServiceName(),
+                            newProfile.getAbout() == null ? "" : newProfile.getAbout(),
+                            newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(),
+                            Optional.absent(),
+                            streamDetails);
+        }
+
+        if (avatar != null) {
+            if (avatar.isPresent()) {
+                avatarStore.storeProfileAvatar(account.getSelfAddress(),
+                        outputStream -> IOUtils.copyFileToStream(avatar.get(), outputStream));
+            } else {
+                avatarStore.deleteProfileAvatar(account.getSelfAddress());
+            }
+        }
+        account.getProfileStore().storeProfile(account.getSelfRecipientId(), newProfile);
+    }
+
+    private final Set<RecipientId> pendingProfileRequest = new HashSet<>();
+
+    private Profile getRecipientProfile(RecipientId recipientId, boolean force) {
+        var profile = account.getProfileStore().getProfile(recipientId);
+
+        var now = System.currentTimeMillis();
+        // Profiles are cached for 24h before retrieving them again, unless forced
+        if (!force && profile != null && now - profile.getLastUpdateTimestamp() < 24 * 60 * 60 * 1000) {
+            return profile;
+        }
+
+        synchronized (pendingProfileRequest) {
+            if (pendingProfileRequest.contains(recipientId)) {
+                return profile;
+            }
+            pendingProfileRequest.add(recipientId);
+        }
+        final SignalServiceProfile encryptedProfile;
+        try {
+            encryptedProfile = retrieveEncryptedProfile(recipientId);
+        } finally {
+            synchronized (pendingProfileRequest) {
+                pendingProfileRequest.remove(recipientId);
+            }
+        }
+        if (encryptedProfile == null) {
+            return null;
+        }
+
+        profile = decryptProfileIfKeyKnown(recipientId, encryptedProfile);
+        account.getProfileStore().storeProfile(recipientId, profile);
+
+        return profile;
+    }
+
+    private Profile decryptProfileIfKeyKnown(
+            final RecipientId recipientId, final SignalServiceProfile encryptedProfile
+    ) {
+        var profileKey = account.getProfileStore().getProfileKey(recipientId);
+        if (profileKey == null) {
+            return new Profile(System.currentTimeMillis(),
+                    null,
+                    null,
+                    null,
+                    null,
+                    ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null),
+                    ProfileUtils.getCapabilities(encryptedProfile));
+        }
+
+        return decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile);
+    }
+
+    private SignalServiceProfile retrieveEncryptedProfile(RecipientId recipientId) {
+        try {
+            return retrieveProfileAndCredential(recipientId, SignalServiceProfile.RequestType.PROFILE).getProfile();
+        } catch (IOException e) {
+            logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
+            return null;
+        }
+    }
+
+    private SignalServiceProfile retrieveProfileSync(String username) throws IOException {
+        return messageReceiverProvider.getMessageReceiver().retrieveProfileByUsername(username, Optional.absent());
+    }
+
+    private ProfileAndCredential retrieveProfileAndCredential(
+            final RecipientId recipientId, final SignalServiceProfile.RequestType requestType
+    ) throws IOException {
+        final var profileAndCredential = retrieveProfileSync(recipientId, requestType);
+        final var profile = profileAndCredential.getProfile();
+
+        try {
+            var newIdentity = account.getIdentityKeyStore()
+                    .saveIdentity(recipientId,
+                            new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())),
+                            new Date());
+
+            if (newIdentity) {
+                account.getSessionStore().archiveSessions(recipientId);
+            }
+        } catch (InvalidKeyException ignored) {
+            logger.warn("Got invalid identity key in profile for {}",
+                    addressResolver.resolveSignalServiceAddress(recipientId).getIdentifier());
+        }
+        return profileAndCredential;
+    }
+
+    private Profile decryptProfileAndDownloadAvatar(
+            final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
+    ) {
+        if (encryptedProfile.getAvatar() != null) {
+            downloadProfileAvatar(addressResolver.resolveSignalServiceAddress(recipientId),
+                    encryptedProfile.getAvatar(),
+                    profileKey);
+        }
+
+        return ProfileUtils.decryptProfile(profileKey, encryptedProfile);
+    }
+
+    private ProfileAndCredential retrieveProfileSync(
             RecipientId recipientId, SignalServiceProfile.RequestType requestType
     ) throws IOException {
         try {
             RecipientId recipientId, SignalServiceProfile.RequestType requestType
     ) throws IOException {
         try {
@@ -58,11 +271,7 @@ public final class ProfileHelper {
         }
     }
 
         }
     }
 
-    public SignalServiceProfile retrieveProfileSync(String username) throws IOException {
-        return messageReceiverProvider.getMessageReceiver().retrieveProfileByUsername(username, Optional.absent());
-    }
-
-    public Single<ProfileAndCredential> retrieveProfile(
+    private Single<ProfileAndCredential> retrieveProfile(
             RecipientId recipientId, SignalServiceProfile.RequestType requestType
     ) throws IOException {
         var unidentifiedAccess = getUnidentifiedAccess(recipientId);
             RecipientId recipientId, SignalServiceProfile.RequestType requestType
     ) throws IOException {
         var unidentifiedAccess = getUnidentifiedAccess(recipientId);
@@ -106,6 +315,42 @@ public final class ProfileHelper {
         });
     }
 
         });
     }
 
+    private void downloadProfileAvatar(
+            SignalServiceAddress address, String avatarPath, ProfileKey profileKey
+    ) {
+        try {
+            avatarStore.storeProfileAvatar(address,
+                    outputStream -> retrieveProfileAvatar(avatarPath, profileKey, outputStream));
+        } catch (Throwable e) {
+            if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) {
+                Thread.currentThread().interrupt();
+            }
+            logger.warn("Failed to download profile avatar, ignoring: {}", e.getMessage());
+        }
+    }
+
+    private void retrieveProfileAvatar(
+            String avatarPath, ProfileKey profileKey, OutputStream outputStream
+    ) throws IOException {
+        var tmpFile = IOUtils.createTempFile();
+        try (var input = dependencies.getMessageReceiver()
+                .retrieveProfileAvatar(avatarPath,
+                        tmpFile,
+                        profileKey,
+                        ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
+            // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
+            IOUtils.copyStream(input, outputStream, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE);
+        } finally {
+            try {
+                Files.delete(tmpFile.toPath());
+            } catch (IOException e) {
+                logger.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}",
+                        tmpFile,
+                        e.getMessage());
+            }
+        }
+    }
+
     private Optional<UnidentifiedAccess> getUnidentifiedAccess(RecipientId recipientId) {
         var unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipientId);
 
     private Optional<UnidentifiedAccess> getUnidentifiedAccess(RecipientId recipientId) {
         var unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipientId);