From 2b1ffac56406bfff50662fd6c684b6d283e22808 Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Tue, 7 Apr 2020 17:55:33 -0300 Subject: [PATCH] Groups V2 avatar download job. --- .../securesms/database/GroupDatabase.java | 2 +- .../dependencies/ApplicationDependencies.java | 11 ++ .../groups/GroupV1MessageProcessor.java | 4 +- .../WorkManagerFactoryMappings.java | 4 +- ...ob.java => AvatarGroupsV1DownloadJob.java} | 18 ++- .../jobs/AvatarGroupsV2DownloadJob.java | 131 ++++++++++++++++++ .../securesms/jobs/JobManagerFactories.java | 3 +- 7 files changed, 157 insertions(+), 16 deletions(-) rename app/src/main/java/org/thoughtcrime/securesms/jobs/{AvatarDownloadJob.java => AvatarGroupsV1DownloadJob.java} (87%) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV2DownloadJob.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index de207146ce..82b00a16c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -380,7 +380,7 @@ public final class GroupDatabase extends Database { /** * Used to bust the Glide cache when an avatar changes. */ - public void onAvatarUpdated(@NonNull GroupId.V1 groupId, boolean hasAvatar) { + public void onAvatarUpdated(@NonNull GroupId.Push groupId, boolean hasAvatar) { ContentValues contentValues = new ContentValues(1); contentValues.put(AVATAR_ID, hasAvatar ? Math.abs(new SecureRandom().nextLong()) : 0); diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java index f76eb29970..53b1286d96 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -46,6 +46,7 @@ public class ApplicationDependencies { private static FrameRateTracker frameRateTracker; private static KeyValueStore keyValueStore; private static MegaphoneRepository megaphoneRepository; + private static GroupsV2Operations groupsV2Operations; public static synchronized void init(@NonNull Application application, @NonNull Provider provider) { if (ApplicationDependencies.application != null || ApplicationDependencies.provider != null) { @@ -71,6 +72,16 @@ public class ApplicationDependencies { return accountManager; } + public static synchronized @NonNull GroupsV2Operations getGroupsV2Operations() { + assertInitialization(); + + if (groupsV2Operations == null) { + groupsV2Operations = provider.provideGroupsV2Operations(); + } + + return groupsV2Operations; + } + public static synchronized @NonNull KeyBackupService getKeyBackupService() { return getSignalServiceAccountManager().getKeyBackupService(IasKeyStore.getIasKeyStore(application), BuildConfig.KBS_ENCLAVE_NAME, diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java index d189c407bd..9d189ad01f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java @@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.jobs.AvatarDownloadJob; +import org.thoughtcrime.securesms.jobs.AvatarGroupsV1DownloadJob; import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.MmsException; @@ -228,7 +228,7 @@ public final class GroupV1MessageProcessor { { if (group.getAvatar().isPresent()) { ApplicationDependencies.getJobManager() - .add(new AvatarDownloadJob(GroupId.v1(group.getGroupId()))); + .add(new AvatarGroupsV1DownloadJob(GroupId.v1(group.getGroupId()))); } try { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/workmanager/WorkManagerFactoryMappings.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/workmanager/WorkManagerFactoryMappings.java index 3ab1290823..5322ee1c46 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/workmanager/WorkManagerFactoryMappings.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/workmanager/WorkManagerFactoryMappings.java @@ -5,7 +5,7 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; import org.thoughtcrime.securesms.jobs.AttachmentUploadJob; -import org.thoughtcrime.securesms.jobs.AvatarDownloadJob; +import org.thoughtcrime.securesms.jobs.AvatarGroupsV1DownloadJob; import org.thoughtcrime.securesms.jobs.CleanPreKeysJob; import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob; import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; @@ -54,7 +54,7 @@ public class WorkManagerFactoryMappings { private static final Map FACTORY_MAP = new HashMap() {{ put(AttachmentDownloadJob.class.getName(), AttachmentDownloadJob.KEY); put(AttachmentUploadJob.class.getName(), AttachmentUploadJob.KEY); - put(AvatarDownloadJob.class.getName(), AvatarDownloadJob.KEY); + put("AvatarDownloadJob", AvatarGroupsV1DownloadJob.KEY); put(CleanPreKeysJob.class.getName(), CleanPreKeysJob.KEY); put(CreateSignedPreKeyJob.class.getName(), CreateSignedPreKeyJob.KEY); put(DirectoryRefreshJob.class.getName(), DirectoryRefreshJob.KEY); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV1DownloadJob.java similarity index 87% rename from app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java rename to app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV1DownloadJob.java index 0131a9ffe5..9403f7c616 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV1DownloadJob.java @@ -24,19 +24,17 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; -public class AvatarDownloadJob extends BaseJob { +public final class AvatarGroupsV1DownloadJob extends BaseJob { public static final String KEY = "AvatarDownloadJob"; - private static final String TAG = AvatarDownloadJob.class.getSimpleName(); - - private static final int MAX_AVATAR_SIZE = 20 * 1024 * 1024; + private static final String TAG = Log.tag(AvatarGroupsV1DownloadJob.class); private static final String KEY_GROUP_ID = "group_id"; - private @NonNull GroupId.V1 groupId; + @NonNull private final GroupId.V1 groupId; - public AvatarDownloadJob(@NonNull GroupId.V1 groupId) { + public AvatarGroupsV1DownloadJob(@NonNull GroupId.V1 groupId) { this(new Job.Parameters.Builder() .addConstraint(NetworkConstraint.KEY) .setMaxAttempts(10) @@ -44,7 +42,7 @@ public class AvatarDownloadJob extends BaseJob { groupId); } - private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull GroupId.V1 groupId) { + private AvatarGroupsV1DownloadJob(@NonNull Job.Parameters parameters, @NonNull GroupId.V1 groupId) { super(parameters); this.groupId = groupId; } @@ -111,10 +109,10 @@ public class AvatarDownloadJob extends BaseJob { return false; } - public static final class Factory implements Job.Factory { + public static final class Factory implements Job.Factory { @Override - public @NonNull AvatarDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) { - return new AvatarDownloadJob(parameters, GroupId.parse(data.getString(KEY_GROUP_ID)).requireV1()); + public @NonNull AvatarGroupsV1DownloadJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new AvatarGroupsV1DownloadJob(parameters, GroupId.parse(data.getString(KEY_GROUP_ID)).requireV1()); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV2DownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV2DownloadJob.java new file mode 100644 index 0000000000..6accdfbf4b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV2DownloadJob.java @@ -0,0 +1,131 @@ +package org.thoughtcrime.securesms.jobs; + +import androidx.annotation.NonNull; + +import org.signal.zkgroup.VerificationFailedException; +import org.signal.zkgroup.groups.GroupSecretParams; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; +import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.groups.GroupId; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.profiles.AvatarHelper; +import org.thoughtcrime.securesms.util.ByteUnit; +import org.thoughtcrime.securesms.util.Util; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; +import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +public final class AvatarGroupsV2DownloadJob extends BaseJob { + + public static final String KEY = "AvatarGroupsV2DownloadJob"; + + private static final String TAG = Log.tag(AvatarGroupsV2DownloadJob.class); + + private static final long AVATAR_DOWNLOAD_FAIL_SAFE_MAX_SIZE = ByteUnit.MEGABYTES.toBytes(5); + + private static final String KEY_GROUP_ID = "group_id"; + private static final String CDN_KEY = "cdn_key"; + + private final GroupId.V2 groupId; + private final String cdnKey; + + public AvatarGroupsV2DownloadJob(@NonNull GroupId.V2 groupId, @NonNull String cdnKey) { + this(new Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setQueue("AvatarGroupsV2DownloadJob::" + groupId) + .setMaxAttempts(10) + .build(), + groupId, + cdnKey); + } + + private AvatarGroupsV2DownloadJob(@NonNull Parameters parameters, @NonNull GroupId.V2 groupId, @NonNull String cdnKey) { + super(parameters); + this.groupId = groupId; + this.cdnKey = cdnKey; + } + + @Override + public @NonNull Data serialize() { + return new Data.Builder() + .putString(KEY_GROUP_ID, groupId.toString()) + .putString(CDN_KEY, cdnKey) + .build(); + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public void onRun() throws IOException { + GroupDatabase database = DatabaseFactory.getGroupDatabase(context); + Optional record = database.getGroup(groupId); + File attachment = null; + + try { + if (!record.isPresent()) { + Log.w(TAG, "Cannot download avatar for unknown group"); + return; + } + + attachment = File.createTempFile("avatar", "gv2", context.getCacheDir()); + attachment.deleteOnExit(); + + SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver(); + byte[] encryptedData; + + try (FileInputStream inputStream = receiver.retrieveGroupsV2ProfileAvatar(cdnKey, attachment, AVATAR_DOWNLOAD_FAIL_SAFE_MAX_SIZE)) { + + encryptedData = new byte[(int) attachment.length()]; + + Util.readFully(inputStream, encryptedData); + + GroupsV2Operations operations = ApplicationDependencies.getGroupsV2Operations(); + GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(record.get().requireV2GroupProperties().getGroupMasterKey()); + GroupsV2Operations.GroupOperations groupOperations = operations.forGroup(groupSecretParams); + byte[] decryptedAvatar = groupOperations.decryptAvatar(encryptedData); + + AvatarHelper.setAvatar(context, record.get().getRecipientId(), decryptedAvatar != null ? new ByteArrayInputStream(decryptedAvatar) : null); + database.onAvatarUpdated(groupId, true); + } + + } catch (NonSuccessfulResponseCodeException | VerificationFailedException e) { + Log.w(TAG, e); + } finally { + if (attachment != null && attachment.exists()) + if (!attachment.delete()) { + Log.w(TAG, "Unable to delete temp avatar file"); + } + } + } + + @Override + public void onFailure() {} + + @Override + public boolean onShouldRetry(@NonNull Exception exception) { + return exception instanceof IOException; + } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull AvatarGroupsV2DownloadJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new AvatarGroupsV2DownloadJob(parameters, + GroupId.parse(data.getString(KEY_GROUP_ID)).requireV2(), + data.getString(CDN_KEY)); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 413a1202f2..0f4c496f93 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -49,7 +49,8 @@ public final class JobManagerFactories { put(AttachmentUploadJob.KEY, new AttachmentUploadJob.Factory()); put(AttachmentMarkUploadedJob.KEY, new AttachmentMarkUploadedJob.Factory()); put(AttachmentCompressionJob.KEY, new AttachmentCompressionJob.Factory()); - put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory()); + put(AvatarGroupsV1DownloadJob.KEY, new AvatarGroupsV1DownloadJob.Factory()); + put(AvatarGroupsV2DownloadJob.KEY, new AvatarGroupsV2DownloadJob.Factory()); put(CleanPreKeysJob.KEY, new CleanPreKeysJob.Factory()); put(CreateSignedPreKeyJob.KEY, new CreateSignedPreKeyJob.Factory()); put(DirectoryRefreshJob.KEY, new DirectoryRefreshJob.Factory());