From: AsamK Date: Thu, 10 Sep 2020 08:19:59 +0000 (+0200) Subject: Retrieve avatar profile image X-Git-Tag: v0.6.9~4 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/0f3aa2251962f240818169bc7b8ab98c61f02020 Retrieve avatar profile image --- diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 73c2b5c7..31f2d9d6 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -142,6 +142,8 @@ 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(); @@ -287,7 +289,7 @@ 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, discoverableByPhoneNumber); + accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities, discoverableByPhoneNumber); } public void setProfile(String name, File avatar) throws IOException { @@ -372,7 +374,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, discoverableByPhoneNumber); + 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() @@ -440,7 +442,27 @@ public class Manager implements Closeable { } private SignalProfile getRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess, ProfileKey profileKey) throws IOException { - return decryptProfile(getEncryptedRecipientProfile(address, unidentifiedAccess), profileKey); + final SignalServiceProfile encryptedProfile = getEncryptedRecipientProfile(address, unidentifiedAccess); + + File avatarFile = null; + try { + avatarFile = encryptedProfile.getAvatar() == null ? null : retrieveProfileAvatar(address, encryptedProfile.getAvatar(), profileKey); + } catch (AssertionError e) { + System.err.println("Failed to retrieve profile avatar: " + 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; + } } private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { @@ -944,21 +966,6 @@ 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) { @@ -1718,6 +1725,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()); } @@ -1743,17 +1773,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()); diff --git a/src/main/java/org/asamk/signal/manager/ServiceConfig.java b/src/main/java/org/asamk/signal/manager/ServiceConfig.java index f8394ada..a8b0c5b6 100644 --- a/src/main/java/org/asamk/signal/manager/ServiceConfig.java +++ b/src/main/java/org/asamk/signal/manager/ServiceConfig.java @@ -26,6 +26,7 @@ public class ServiceConfig { final static int PREKEY_MINIMUM_COUNT = 20; final static int PREKEY_BATCH_SIZE = 100; final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; + final static long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024; private final static String URL = "https://textsecure-service.whispersystems.org"; private final static String CDN_URL = "https://cdn.signal.org"; diff --git a/src/main/java/org/asamk/signal/manager/SignalProfile.java b/src/main/java/org/asamk/signal/manager/SignalProfile.java index 4f529c0d..1217793c 100644 --- a/src/main/java/org/asamk/signal/manager/SignalProfile.java +++ b/src/main/java/org/asamk/signal/manager/SignalProfile.java @@ -1,23 +1,30 @@ package org.asamk.signal.manager; +import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; + +import java.io.File; + public class SignalProfile { private final String identityKey; private final String name; - private final String avatar; + private final File avatarFile; private final String unidentifiedAccess; private final boolean unrestrictedUnidentifiedAccess; - public SignalProfile(final String identityKey, final String name, final String avatar, final String unidentifiedAccess, final boolean unrestrictedUnidentifiedAccess) { + private final SignalServiceProfile.Capabilities capabilities; + + public SignalProfile(final String identityKey, final String name, final File avatarFile, final String unidentifiedAccess, final boolean unrestrictedUnidentifiedAccess, final SignalServiceProfile.Capabilities capabilities) { this.identityKey = identityKey; this.name = name; - this.avatar = avatar; + this.avatarFile = avatarFile; this.unidentifiedAccess = unidentifiedAccess; this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess; + this.capabilities = capabilities; } public String getIdentityKey() { @@ -28,8 +35,8 @@ public class SignalProfile { return name; } - public String getAvatar() { - return avatar; + public File getAvatarFile() { + return avatarFile; } public String getUnidentifiedAccess() { @@ -40,14 +47,19 @@ public class SignalProfile { return unrestrictedUnidentifiedAccess; } + public SignalServiceProfile.Capabilities getCapabilities() { + return capabilities; + } + @Override public String toString() { return "SignalProfile{" + "identityKey='" + identityKey + '\'' + ", name='" + name + '\'' + - ", avatar='" + avatar + '\'' + + ", avatarFile=" + avatarFile + ", unidentifiedAccess='" + unidentifiedAccess + '\'' + ", unrestrictedUnidentifiedAccess=" + unrestrictedUnidentifiedAccess + + ", capabilities=" + capabilities + '}'; } } diff --git a/src/main/java/org/asamk/signal/util/IOUtils.java b/src/main/java/org/asamk/signal/util/IOUtils.java index f21c1572..1163d079 100644 --- a/src/main/java/org/asamk/signal/util/IOUtils.java +++ b/src/main/java/org/asamk/signal/util/IOUtils.java @@ -4,8 +4,10 @@ import org.whispersystems.signalservice.internal.util.Util; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.StringWriter; import java.nio.charset.Charset; import java.nio.file.Files; @@ -77,4 +79,19 @@ public class IOUtils { return System.getProperty("user.home") + "/.local/share"; } + + public static void copyStreamToFile(InputStream input, File outputFile) throws IOException { + copyStreamToFile(input, outputFile, 8192); + } + + public static void copyStreamToFile(InputStream input, File outputFile, int bufferSize) throws IOException { + try (OutputStream output = new FileOutputStream(outputFile)) { + byte[] buffer = new byte[bufferSize]; + int read; + + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + } + } }