]> nmode's Git Repositories - signal-cli/blobdiff - src/main/java/org/asamk/signal/manager/Manager.java
Use StandardCharsets.UTF_8
[signal-cli] / src / main / java / org / asamk / signal / manager / Manager.java
index e57bd531f48b9745638100616e1de20741ff6fbc..a4127b75c369d6ce0f0c1b0ba27097f9908001ac 100644 (file)
@@ -22,6 +22,8 @@ import org.asamk.signal.storage.SignalAccount;
 import org.asamk.signal.storage.contacts.ContactInfo;
 import org.asamk.signal.storage.groups.GroupInfo;
 import org.asamk.signal.storage.groups.JsonGroupStore;
+import org.asamk.signal.storage.profiles.SignalProfile;
+import org.asamk.signal.storage.profiles.SignalProfileEntry;
 import org.asamk.signal.storage.protocol.JsonIdentityKeyStore;
 import org.asamk.signal.util.IOUtils;
 import org.asamk.signal.util.Util;
@@ -38,7 +40,6 @@ import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
 import org.signal.libsignal.metadata.SelfSendException;
 import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
 import org.signal.zkgroup.InvalidInputException;
-import org.signal.zkgroup.VerificationFailedException;
 import org.signal.zkgroup.profiles.ClientZkProfileOperations;
 import org.signal.zkgroup.profiles.ProfileKey;
 import org.whispersystems.libsignal.IdentityKey;
@@ -65,6 +66,8 @@ import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
 import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
+import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
+import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
 import org.whispersystems.signalservice.api.messages.SendMessageResult;
 import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
@@ -106,6 +109,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf
 import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
 import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
 import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
+import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
 import org.whispersystems.signalservice.internal.util.Hex;
 import org.whispersystems.util.Base64;
 
@@ -120,6 +124,7 @@ import java.io.OutputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
@@ -135,12 +140,16 @@ import java.util.Locale;
 import java.util.Objects;
 import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.stream.Collectors;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 
+import static org.asamk.signal.manager.ServiceConfig.capabilities;
+
 public class Manager implements Closeable {
 
     private final SleepTimer timer = new UptimeSleepTimer();
@@ -152,6 +161,7 @@ public class Manager implements Closeable {
     private SignalServiceAccountManager accountManager;
     private SignalServiceMessagePipe messagePipe = null;
     private SignalServiceMessagePipe unidentifiedMessagePipe = null;
+    private boolean discoverableByPhoneNumber = true;
 
     public Manager(SignalAccount account, PathConfig pathConfig, SignalServiceConfiguration serviceConfiguration, String userAgent) {
         this.account = account;
@@ -172,7 +182,17 @@ public class Manager implements Closeable {
     }
 
     private SignalServiceAccountManager createSignalServiceAccountManager() {
-        return new SignalServiceAccountManager(serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), userAgent, timer);
+        GroupsV2Operations groupsV2Operations;
+        try {
+            groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceConfiguration));
+        } catch (Throwable ignored) {
+            groupsV2Operations = null;
+        }
+        return new SignalServiceAccountManager(serviceConfiguration,
+                new DynamicCredentialsProvider(account.getUuid(), account.getUsername(), account.getPassword(), null, account.getDeviceId()),
+                userAgent,
+                groupsV2Operations,
+                timer);
     }
 
     private IdentityKeyPair getIdentityKeyPair() {
@@ -248,6 +268,21 @@ public class Manager implements Closeable {
             account.setProfileKey(KeyUtils.createProfileKey());
             account.save();
         }
+        // Store profile keys only in profile store
+        for (ContactInfo contact : account.getContactStore().getContacts()) {
+            String profileKeyString = contact.profileKey;
+            if (profileKeyString == null) {
+                continue;
+            }
+            final ProfileKey profileKey;
+            try {
+                profileKey = new ProfileKey(Base64.decode(profileKeyString));
+            } catch (InvalidInputException | IOException e) {
+                continue;
+            }
+            contact.profileKey = null;
+            account.getProfileStore().storeProfileKey(contact.getAddress(), profileKey);
+        }
     }
 
     public void checkAccountState() throws IOException {
@@ -285,21 +320,13 @@ public class Manager implements Closeable {
     }
 
     public void updateAccountAttributes() throws IOException {
-        accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, ServiceConfig.capabilities);
+        accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities, discoverableByPhoneNumber);
     }
 
-    public void setProfileName(String name) throws IOException {
-        accountManager.setProfileName(account.getProfileKey(), name);
-    }
-
-    public void setProfileAvatar(File avatar) throws IOException {
-        final StreamDetails streamDetails = Utils.createStreamDetailsFromFile(avatar);
-        accountManager.setProfileAvatar(account.getProfileKey(), streamDetails);
-        streamDetails.getStream().close();
-    }
-
-    public void removeProfileAvatar() throws IOException {
-        accountManager.setProfileAvatar(account.getProfileKey(), null);
+    public void setProfile(String name, File avatar) throws IOException {
+        try (final StreamDetails streamDetails = avatar == null ? null : Utils.createStreamDetailsFromFile(avatar)) {
+            accountManager.setVersionedProfile(account.getUuid(), account.getProfileKey(), name, streamDetails);
+        }
     }
 
     public void unregister() throws IOException {
@@ -378,7 +405,7 @@ public class Manager implements Closeable {
         verificationCode = verificationCode.replace("-", "");
         account.setSignalingKey(KeyUtils.createSignalingKey());
         // TODO make unrestricted unidentified access configurable
-        VerifyAccountResponse response = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, ServiceConfig.capabilities);
+        VerifyAccountResponse response = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, capabilities, discoverableByPhoneNumber);
 
         UUID uuid = UuidUtil.parseOrNull(response.getUuid());
         // TODO response.isStorageCapable()
@@ -420,27 +447,63 @@ public class Manager implements Closeable {
     private SignalServiceMessageSender getMessageSender() {
         // TODO implement ZkGroup support
         final ClientZkProfileOperations clientZkProfileOperations = null;
-        final boolean attachmentsV3 = false;
+        final ExecutorService executor = null;
         return new SignalServiceMessageSender(serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(),
-                account.getDeviceId(), account.getSignalProtocolStore(), userAgent, account.isMultiDevice(), attachmentsV3, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent(), clientZkProfileOperations);
+                account.getDeviceId(), account.getSignalProtocolStore(), userAgent, account.isMultiDevice(), Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent(), clientZkProfileOperations, executor, ServiceConfig.MAX_ENVELOPE_SIZE);
     }
 
-    private SignalServiceProfile getRecipientProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess) throws IOException {
+    private SignalServiceProfile getEncryptedRecipientProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess) throws IOException {
         SignalServiceMessagePipe pipe = unidentifiedMessagePipe != null && unidentifiedAccess.isPresent() ? unidentifiedMessagePipe
                 : messagePipe;
 
         if (pipe != null) {
             try {
-                return pipe.getProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).getProfile();
-            } catch (IOException ignored) {
+                return pipe.getProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS).getProfile();
+            } catch (IOException | InterruptedException | ExecutionException | TimeoutException ignored) {
             }
         }
 
         SignalServiceMessageReceiver receiver = getMessageReceiver();
         try {
-            return receiver.retrieveProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).getProfile();
-        } catch (VerificationFailedException e) {
-            throw new AssertionError(e);
+            return receiver.retrieveProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS).getProfile();
+        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+            throw new IOException("Failed to retrieve profile", e);
+        }
+    }
+
+    private SignalProfile getRecipientProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess, ProfileKey profileKey) throws IOException {
+        SignalProfileEntry profileEntry = account.getProfileStore().getProfile(address);
+        long now = new Date().getTime();
+        // Profiles are cache for 24h before retrieving them again
+        if (profileEntry == null || profileEntry.getProfile() == null || now - profileEntry.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000) {
+            SignalProfile profile = retrieveRecipientProfile(address, unidentifiedAccess, profileKey);
+            account.getProfileStore().updateProfile(address, profileKey, now, profile);
+            return profile;
+        }
+        return profileEntry.getProfile();
+    }
+
+    private SignalProfile retrieveRecipientProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess, ProfileKey profileKey) throws IOException {
+        final SignalServiceProfile encryptedProfile = getEncryptedRecipientProfile(address, unidentifiedAccess);
+
+        File avatarFile = null;
+        try {
+            avatarFile = encryptedProfile.getAvatar() == null ? null : retrieveProfileAvatar(address, encryptedProfile.getAvatar(), profileKey);
+        } catch (Throwable e) {
+            System.err.println("Failed to retrieve profile avatar, ignoring: " + e.getMessage());
+        }
+
+        ProfileCipher profileCipher = new ProfileCipher(profileKey);
+        try {
+            return new SignalProfile(
+                    encryptedProfile.getIdentityKey(),
+                    encryptedProfile.getName() == null ? null : new String(profileCipher.decryptName(Base64.decode(encryptedProfile.getName()))),
+                    avatarFile,
+                    encryptedProfile.getUnidentifiedAccess() == null || !profileCipher.verifyUnidentifiedAccess(Base64.decode(encryptedProfile.getUnidentifiedAccess())) ? null : encryptedProfile.getUnidentifiedAccess(),
+                    encryptedProfile.isUnrestrictedUnidentifiedAccess(),
+                    encryptedProfile.getCapabilities());
+        } catch (InvalidCiphertextException e) {
+            return null;
         }
     }
 
@@ -558,9 +621,7 @@ public class Manager implements Closeable {
                 for (ContactTokenDetails contact : contacts) {
                     newE164Members.remove(contact.getNumber());
                 }
-                System.err.println("Failed to add members " + Util.join(", ", newE164Members) + " to group: Not registered on Signal");
-                System.err.println("Aborting…");
-                System.exit(1);
+                throw new IOException("Failed to add members " + Util.join(", ", newE164Members) + " to group: Not registered on Signal");
             }
 
             g.addMembers(members);
@@ -701,9 +762,6 @@ public class Manager implements Closeable {
         ContactInfo contact = account.getContactStore().getContact(address);
         if (contact == null) {
             contact = new ContactInfo(address);
-            System.err.println("Add contact " + contact.number + " named " + name);
-        } else {
-            System.err.println("Updating contact " + contact.number + " name " + contact.name + " -> " + name);
         }
         contact.name = name;
         account.getContactStore().updateContact(contact);
@@ -718,9 +776,6 @@ public class Manager implements Closeable {
         ContactInfo contact = account.getContactStore().getContact(address);
         if (contact == null) {
             contact = new ContactInfo(address);
-            System.err.println("Adding and " + (blocked ? "blocking" : "unblocking") + " contact " + address.getNumber().orNull());
-        } else {
-            System.err.println((blocked ? "Blocking" : "Unblocking") + " contact " + address.getNumber().orNull());
         }
         contact.blocked = blocked;
         account.getContactStore().updateContact(contact);
@@ -731,12 +786,11 @@ public class Manager implements Closeable {
         GroupInfo group = getGroup(groupId);
         if (group == null) {
             throw new GroupNotFoundException(groupId);
-        } else {
-            System.err.println((blocked ? "Blocking" : "Unblocking") + " group " + Base64.encodeBytes(groupId));
-            group.blocked = blocked;
-            account.getGroupStore().updateGroup(group);
-            account.save();
         }
+
+        group.blocked = blocked;
+        account.getGroupStore().updateGroup(group);
+        account.save();
     }
 
     public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
@@ -746,7 +800,7 @@ public class Manager implements Closeable {
         if (name.isEmpty()) {
             name = null;
         }
-        if (members.size() == 0) {
+        if (members.isEmpty()) {
             members = null;
         }
         if (avatar.isEmpty()) {
@@ -804,7 +858,7 @@ public class Manager implements Closeable {
         String packId = messageSender.uploadStickerManifest(manifest, packKey);
 
         try {
-            return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder.encode(packId, "utf-8") + "&pack_key=" + URLEncoder.encode(Hex.toStringCondensed(packKey), "utf-8"))
+            return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder.encode(packId, StandardCharsets.UTF_8) + "&pack_key=" + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8))
                     .toString();
         } catch (URISyntaxException e) {
             throw new AssertionError(e);
@@ -847,7 +901,8 @@ public class Manager implements Closeable {
                 throw new StickerPackInvalidException("Could not find find " + sticker.file);
             }
 
-            StickerInfo stickerInfo = new StickerInfo(data.first(), data.second(), Optional.fromNullable(sticker.emoji).or(""));
+            String contentType = Utils.getFileMimeType(new File(sticker.file), null);
+            StickerInfo stickerInfo = new StickerInfo(data.first(), data.second(), Optional.fromNullable(sticker.emoji).or(""), contentType);
             stickers.add(stickerInfo);
         }
 
@@ -864,7 +919,8 @@ public class Manager implements Closeable {
                 throw new StickerPackInvalidException("Could not find find " + pack.cover.file);
             }
 
-            cover = new StickerInfo(data.first(), data.second(), Optional.fromNullable(pack.cover.emoji).or(""));
+            String contentType = Utils.getFileMimeType(new File(pack.cover.file), null);
+            cover = new StickerInfo(data.first(), data.second(), Optional.fromNullable(pack.cover.emoji).or(""), contentType);
         }
 
         return new SignalServiceStickerManifestUpload(
@@ -936,10 +992,10 @@ public class Manager implements Closeable {
 
     private byte[] getSenderCertificate() {
         // TODO support UUID capable sender certificates
-        // byte[] certificate = accountManager.getSenderCertificate();
+        // byte[] certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy();
         byte[] certificate;
         try {
-            certificate = accountManager.getSenderCertificateLegacy();
+            certificate = accountManager.getSenderCertificate();
         } catch (IOException e) {
             System.err.println("Failed to get sender certificate: " + e);
             return null;
@@ -952,35 +1008,14 @@ public class Manager implements Closeable {
         return UnidentifiedAccess.deriveAccessKeyFrom(account.getProfileKey());
     }
 
-    private static SignalProfile decryptProfile(SignalServiceProfile encryptedProfile, ProfileKey profileKey) throws IOException {
-        ProfileCipher profileCipher = new ProfileCipher(profileKey);
-        try {
-            return new SignalProfile(
-                    encryptedProfile.getIdentityKey(),
-                    encryptedProfile.getName() == null ? null : new String(profileCipher.decryptName(Base64.decode(encryptedProfile.getName()))),
-                    encryptedProfile.getAvatar(),
-                    encryptedProfile.getUnidentifiedAccess() == null || !profileCipher.verifyUnidentifiedAccess(Base64.decode(encryptedProfile.getUnidentifiedAccess())) ? null : encryptedProfile.getUnidentifiedAccess(),
-                    encryptedProfile.isUnrestrictedUnidentifiedAccess()
-            );
-        } catch (InvalidCiphertextException e) {
-            return null;
-        }
-    }
-
     private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) {
-        ContactInfo contact = account.getContactStore().getContact(recipient);
-        if (contact == null || contact.profileKey == null) {
+        ProfileKey theirProfileKey = account.getProfileStore().getProfileKey(recipient);
+        if (theirProfileKey == null) {
             return null;
         }
-        ProfileKey theirProfileKey;
-        try {
-            theirProfileKey = new ProfileKey(Base64.decode(contact.profileKey));
-        } catch (InvalidInputException | IOException e) {
-            throw new AssertionError(e);
-        }
         SignalProfile targetProfile;
         try {
-            targetProfile = decryptProfile(getRecipientProfile(recipient, Optional.absent()), theirProfileKey);
+            targetProfile = getRecipientProfile(recipient, Optional.absent(), theirProfileKey);
         } catch (IOException e) {
             System.err.println("Failed to get recipient profile: " + e);
             return null;
@@ -1172,8 +1207,9 @@ public class Manager implements Closeable {
         SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
 
         try {
+            long startTime = System.currentTimeMillis();
             messageSender.sendMessage(syncMessage, unidentifiedAccess);
-            return SendMessageResult.success(recipient, unidentifiedAccess.isPresent(), false);
+            return SendMessageResult.success(recipient, unidentifiedAccess.isPresent(), false, System.currentTimeMillis() - startTime);
         } catch (UntrustedIdentityException e) {
             account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED);
             return SendMessageResult.identityFailure(recipient, e.getIdentityKey());
@@ -1300,24 +1336,16 @@ public class Manager implements Closeable {
             }
         }
         if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
+            final ProfileKey profileKey;
+            try {
+                profileKey = new ProfileKey(message.getProfileKey().get());
+            } catch (InvalidInputException e) {
+                throw new AssertionError(e);
+            }
             if (source.matches(account.getSelfAddress())) {
-                try {
-                    this.account.setProfileKey(new ProfileKey(message.getProfileKey().get()));
-                } catch (InvalidInputException ignored) {
-                }
-                ContactInfo contact = account.getContactStore().getContact(source);
-                if (contact != null) {
-                    contact.profileKey = Base64.encodeBytes(message.getProfileKey().get());
-                    account.getContactStore().updateContact(contact);
-                }
-            } else {
-                ContactInfo contact = account.getContactStore().getContact(source);
-                if (contact == null) {
-                    contact = new ContactInfo(source);
-                }
-                contact.profileKey = Base64.encodeBytes(message.getProfileKey().get());
-                account.getContactStore().updateContact(contact);
+                this.account.setProfileKey(profileKey);
             }
+            this.account.getProfileStore().storeProfileKey(source, profileKey);
         }
         if (message.getPreviews().isPresent()) {
             final List<SignalServiceDataMessage.Preview> previews = message.getPreviews().get();
@@ -1372,7 +1400,15 @@ public class Manager implements Closeable {
         if (!envelope.isReceipt()) {
             try {
                 content = decryptMessage(envelope);
-            } catch (Exception e) {
+            } catch (org.whispersystems.libsignal.UntrustedIdentityException e) {
+                return;
+            } catch (Exception er) {
+                // All other errors are not recoverable, so delete the cached message
+                try {
+                    Files.delete(fileEntry.toPath());
+                } catch (IOException e) {
+                    System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage());
+                }
                 return;
             }
             List<HandleAction> actions = handleMessage(envelope, content, ignoreAttachments);
@@ -1435,6 +1471,7 @@ public class Manager implements Closeable {
                                 e.printStackTrace();
                             }
                         }
+                        account.save();
                         queuedActions.clear();
                         queuedActions = null;
                     }
@@ -1450,6 +1487,7 @@ public class Manager implements Closeable {
                 System.err.println("Ignoring error: " + e.getMessage());
                 continue;
             }
+
             if (envelope.hasSource()) {
                 // Store uuid if we don't have it already
                 SignalServiceAddress source = envelope.getSourceAddress();
@@ -1484,7 +1522,8 @@ public class Manager implements Closeable {
             if (!(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) {
                 File cacheFile = null;
                 try {
-                    cacheFile = getMessageCacheFile(envelope.getSourceE164().get(), now, envelope.getTimestamp());
+                    String source = envelope.getSourceE164().isPresent() ? envelope.getSourceE164().get() : "";
+                    cacheFile = getMessageCacheFile(source, now, envelope.getTimestamp());
                     Files.delete(cacheFile.toPath());
                     // Try to delete directory if empty
                     new File(getMessageCachePath()).delete();
@@ -1653,7 +1692,7 @@ public class Manager implements Closeable {
                                     contact.color = c.getColor().get();
                                 }
                                 if (c.getProfileKey().isPresent()) {
-                                    contact.profileKey = Base64.encodeBytes(c.getProfileKey().get().serialize());
+                                    account.getProfileStore().storeProfileKey(address, c.getProfileKey().get());
                                 }
                                 if (c.getVerified().isPresent()) {
                                     final VerifiedMessage verifiedMessage = c.getVerified().get();
@@ -1726,6 +1765,29 @@ public class Manager implements Closeable {
         }
     }
 
+    private File getProfileAvatarFile(SignalServiceAddress address) {
+        return new File(pathConfig.getAvatarsPath(), "profile-" + address.getLegacyIdentifier());
+    }
+
+    private File retrieveProfileAvatar(SignalServiceAddress address, String avatarPath, ProfileKey profileKey) throws IOException {
+        IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
+        SignalServiceMessageReceiver receiver = getMessageReceiver();
+        File outputFile = getProfileAvatarFile(address);
+
+        File tmpFile = IOUtils.createTempFile();
+        try (InputStream input = receiver.retrieveProfileAvatar(avatarPath, tmpFile, profileKey, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
+            // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
+            IOUtils.copyStreamToFile(input, outputFile, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE);
+        } finally {
+            try {
+                Files.delete(tmpFile.toPath());
+            } catch (IOException e) {
+                System.err.println("Failed to delete received avatar temp file “" + tmpFile + "”: " + e.getMessage());
+            }
+        }
+        return outputFile;
+    }
+
     public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) {
         return new File(pathConfig.getAttachmentsPath(), attachmentId.toString());
     }
@@ -1751,17 +1813,7 @@ public class Manager implements Closeable {
 
         File tmpFile = IOUtils.createTempFile();
         try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE)) {
-            try (OutputStream output = new FileOutputStream(outputFile)) {
-                byte[] buffer = new byte[4096];
-                int read;
-
-                while ((read = input.read(buffer)) != -1) {
-                    output.write(buffer, 0, read);
-                }
-            } catch (FileNotFoundException e) {
-                e.printStackTrace();
-                return null;
-            }
+            IOUtils.copyStreamToFile(input, outputFile);
         } finally {
             try {
                 Files.delete(tmpFile.toPath());
@@ -1824,11 +1876,7 @@ public class Manager implements Closeable {
                         verifiedMessage = new VerifiedMessage(record.getAddress(), currentIdentity.getIdentityKey(), currentIdentity.getTrustLevel().toVerifiedState(), currentIdentity.getDateAdded().getTime());
                     }
 
-                    ProfileKey profileKey = null;
-                    try {
-                        profileKey = record.profileKey == null ? null : new ProfileKey(Base64.decode(record.profileKey));
-                    } catch (InvalidInputException ignored) {
-                    }
+                    ProfileKey profileKey = account.getProfileStore().getProfileKey(record.getAddress());
                     out.write(new DeviceContact(record.getAddress(), Optional.fromNullable(record.name),
                             createContactAvatarAttachment(record.number), Optional.fromNullable(record.color),
                             Optional.fromNullable(verifiedMessage), Optional.fromNullable(profileKey), record.blocked,