From 77a216b70542dc919243ef8dfe114176bb670142 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Mon, 14 Aug 2017 18:11:13 -0700 Subject: [PATCH] Support for retrieving and storing profile information Initial support for sharing profile keys // FREEBIE --- build.gradle | 6 +- .../components/webrtc/WebRtcCallScreen.java | 2 +- .../contacts/avatars/ContactPhotoFactory.java | 34 +++++- .../securesms/database/DatabaseFactory.java | 9 +- .../database/RecipientPreferenceDatabase.java | 74 +++++++++++- .../SignalCommunicationModule.java | 4 +- .../securesms/jobs/PushDecryptJob.java | 26 +++++ .../securesms/jobs/PushGroupSendJob.java | 2 +- .../securesms/jobs/PushMediaSendJob.java | 3 + .../securesms/jobs/PushSendJob.java | 27 +++++ .../securesms/jobs/PushTextSendJob.java | 3 + .../jobs/RetrieveProfileAvatarJob.java | 109 ++++++++++++++++++ .../securesms/jobs/RetrieveProfileJob.java | 85 +++++++++++--- .../securesms/mms/TextSecureGlideModule.java | 3 + .../profiles/AvatarPhotoUriFetcher.java | 54 +++++++++ .../profiles/AvatarPhotoUriLoader.java | 48 ++++++++ .../recipients/RecipientProvider.java | 1 + 17 files changed, 455 insertions(+), 35 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java create mode 100644 src/org/thoughtcrime/securesms/profiles/AvatarPhotoUriFetcher.java create mode 100644 src/org/thoughtcrime/securesms/profiles/AvatarPhotoUriLoader.java diff --git a/build.gradle b/build.gradle index 12ce72dd8f..14b2330e55 100644 --- a/build.gradle +++ b/build.gradle @@ -63,7 +63,7 @@ dependencies { compile 'org.whispersystems:jobmanager:1.0.2' compile 'org.whispersystems:libpastelog:1.0.7' - compile 'org.whispersystems:signal-service-android:2.6.0' + compile 'org.whispersystems:signal-service-android:2.6.1' compile 'org.whispersystems:webrtc-android:M59-S1' compile "me.leolin:ShortcutBadger:1.1.16" @@ -138,7 +138,7 @@ dependencyVerification { 'com.google.android.exoplayer:exoplayer:955085aa611a8f7cf6c61b88ae03d1a392f4ad94c9bfbc153f3dedb9ffb14718', 'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181', 'org.whispersystems:libpastelog:bb331d9a98240fc139101128ba836c1edec3c40e000597cdbb29ebf4cbf34d88', - 'org.whispersystems:signal-service-android:a25bfa4fd86f2d5f6cedb54d2c917af7b521dc4dda14ee35a3ac7267f1d29c7e', + 'org.whispersystems:signal-service-android:9458c3f05698863f3abacbbcb3dfcbb6e2de26e4b1869e25baccebc1e140c97f', 'org.whispersystems:webrtc-android:de647643afbbea45a26a4f24db75aa10bc8de45426e8eb0d9d563cc10af4f582', 'me.leolin:ShortcutBadger:e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774', 'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb', @@ -173,7 +173,7 @@ dependencyVerification { 'com.google.android.gms:play-services-basement:95dd882c5ffba15b9a99de3fefb05d3a01946623af67454ca00055d222f85a8d', 'com.google.android.gms:play-services-iid:54e919f9957b8b7820da7ee9b83471d00d0cac1cf08ddea8b5b41aea80bb1a70', 'org.whispersystems:signal-protocol-android:5b8acded7f2a40178eb90ab8e8cbfec89d170d91b3ff5e78487d1098df6185a1', - 'org.whispersystems:signal-service-java:6524523dc774943a31784fa088c8185ad32f240e7c5f63245ab1e61081f76a83', + 'org.whispersystems:signal-service-java:8c72b0055ed01fd98430105c872e93c9af7ce18197cd33566ca9ab561e3102be', 'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a', 'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff', 'com.klinkerapps:logger:177e325259a8b111ad6745ec10db5861723c99f402222b80629f576f49408541', diff --git a/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java b/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java index ec96a21c65..16a7da5460 100644 --- a/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java +++ b/src/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java @@ -293,7 +293,7 @@ public class WebRtcCallScreen extends FrameLayout implements RecipientModifiedLi Uri contentUri = ContactsContract.Contacts.lookupContact(context.getContentResolver(), recipient.getContactUri()); windowManager.getDefaultDisplay().getMetrics(metrics); - return ContactPhotoFactory.getContactPhoto(context, contentUri, null, metrics.widthPixels); + return ContactPhotoFactory.getContactPhoto(context, contentUri, recipient.getAddress(), null, metrics.widthPixels); } @Override diff --git a/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java b/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java index ccb54d433e..94cc521bc5 100644 --- a/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java +++ b/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java @@ -13,7 +13,10 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.mms.ContactPhotoUriLoader.ContactPhotoUri; +import org.thoughtcrime.securesms.profiles.AvatarPhotoUriLoader; +import org.thoughtcrime.securesms.profiles.AvatarPhotoUriLoader.AvatarPhotoUri; import java.util.concurrent.ExecutionException; @@ -38,17 +41,18 @@ public class ContactPhotoFactory { return new ResourceContactPhoto(R.drawable.ic_group_white_24dp); } - public static ContactPhoto getContactPhoto(Context context, Uri uri, String name) { + public static ContactPhoto getContactPhoto(@NonNull Context context, @Nullable Uri uri, @NonNull Address address, @Nullable String name) { int targetSize = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size); - return getContactPhoto(context, uri, name, targetSize); + return getContactPhoto(context, uri, address, name, targetSize); } public static ContactPhoto getContactPhoto(@NonNull Context context, @Nullable Uri uri, + @NonNull Address address, @Nullable String name, int targetSize) { - if (uri == null) return getDefaultContactPhoto(name); + if (uri == null) return getSignalAvatarContactPhoto(context, address, name, targetSize); try { Bitmap bitmap = Glide.with(context) @@ -57,7 +61,7 @@ public class ContactPhotoFactory { .centerCrop().into(targetSize, targetSize).get(); return new BitmapContactPhoto(bitmap); } catch (ExecutionException e) { - return getDefaultContactPhoto(name); + return getSignalAvatarContactPhoto(context, address, name, targetSize); } catch (InterruptedException e) { throw new AssertionError(e); } @@ -68,4 +72,26 @@ public class ContactPhotoFactory { return new BitmapContactPhoto(BitmapFactory.decodeByteArray(avatar, 0, avatar.length)); } + + private static ContactPhoto getSignalAvatarContactPhoto(@NonNull Context context, + @NonNull Address address, + @Nullable String name, + int targetSize) + { + try { + Bitmap bitmap = Glide.with(context) + .load(new AvatarPhotoUri(address)) + .asBitmap() + .diskCacheStrategy(DiskCacheStrategy.NONE) + .centerCrop() + .into(targetSize, targetSize) + .get(); + + return new BitmapContactPhoto(bitmap); + } catch (ExecutionException e) { + return getDefaultContactPhoto(name); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + } } diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index f92a6a9c3f..b1075bd3d4 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -103,7 +103,8 @@ public class DatabaseFactory { private static final int NO_MORE_RECIPIENTS_PLURAL = 38; private static final int INTERNAL_DIRECTORY = 39; private static final int INTERNAL_SYSTEM_DISPLAY_NAME = 40; - private static final int DATABASE_VERSION = 40; + private static final int PROFILES = 41; + private static final int DATABASE_VERSION = 41; private static final String DATABASE_NAME = "messages.db"; private static final Object lock = new Object(); @@ -1297,6 +1298,12 @@ public class DatabaseFactory { db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN system_display_name TEXT DEFAULT NULL"); } + if (oldVersion < PROFILES) { + db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN profile_key TEXT DEFAULT NULL"); + db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN signal_profile_name TEXT DEFAULT NULL"); + db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN signal_profile_avatar TEXT DEFAULT NULL"); + } + db.setTransactionSuccessful(); db.endTransaction(); } diff --git a/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java b/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java index b643c86316..b1b28c2b67 100644 --- a/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java +++ b/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java @@ -16,8 +16,10 @@ import org.greenrobot.eventbus.EventBus; import org.thoughtcrime.securesms.color.MaterialColor; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; +import org.thoughtcrime.securesms.util.Base64; import org.whispersystems.libsignal.util.guava.Optional; +import java.io.IOException; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -40,10 +42,14 @@ public class RecipientPreferenceDatabase extends Database { private static final String DEFAULT_SUBSCRIPTION_ID = "default_subscription_id"; private static final String EXPIRE_MESSAGES = "expire_messages"; private static final String REGISTERED = "registered"; + private static final String PROFILE_KEY = "profile_key"; private static final String SYSTEM_DISPLAY_NAME = "system_display_name"; + private static final String SIGNAL_PROFILE_NAME = "signal_profile_name"; + private static final String SIGNAL_PROFILE_AVATAR = "signal_profile_avatar"; private static final String[] RECIPIENT_PROJECTION = new String[] { - BLOCK, NOTIFICATION, VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, EXPIRE_MESSAGES, REGISTERED, SYSTEM_DISPLAY_NAME + BLOCK, NOTIFICATION, VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, EXPIRE_MESSAGES, REGISTERED, + PROFILE_KEY, SYSTEM_DISPLAY_NAME, SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR }; static final List TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION) @@ -81,7 +87,10 @@ public class RecipientPreferenceDatabase extends Database { DEFAULT_SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " + EXPIRE_MESSAGES + " INTEGER DEFAULT 0, " + REGISTERED + " INTEGER DEFAULT 0, " + - SYSTEM_DISPLAY_NAME + " TEXT DEFAULT NULL);"; + SYSTEM_DISPLAY_NAME + " TEXT DEFAULT NULL, " + + PROFILE_KEY + " TEXT DEFAULT NULL, " + + SIGNAL_PROFILE_NAME + " TEXT DEFAULT NULL, " + + SIGNAL_PROFILE_AVATAR + " TEXT DEFAULT NULL);"; public RecipientPreferenceDatabase(Context context, SQLiteOpenHelper databaseHelper) { super(context, databaseHelper); @@ -130,9 +139,13 @@ public class RecipientPreferenceDatabase extends Database { int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID)); int expireMessages = cursor.getInt(cursor.getColumnIndexOrThrow(EXPIRE_MESSAGES)); boolean registered = cursor.getInt(cursor.getColumnIndexOrThrow(REGISTERED)) == 1; - String systemDisplayname = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_DISPLAY_NAME)); + String profileKeyString = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_KEY)); + String systemDisplayName = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_DISPLAY_NAME)); + String signalProfileName = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_NAME)); + String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_AVATAR)); MaterialColor color; + byte[] profileKey = null; try { color = serializedColor == null ? null : MaterialColor.fromSerialized(serializedColor); @@ -141,11 +154,21 @@ public class RecipientPreferenceDatabase extends Database { color = null; } + if (profileKeyString != null) { + try { + profileKey = Base64.decode(profileKeyString); + } catch (IOException e) { + Log.w(TAG, e); + profileKey = null; + } + } + return Optional.of(new RecipientsPreferences(blocked, muteUntil, VibrateState.fromId(vibrateState), notificationUri, color, seenInviteReminder, defaultSubscriptionId, expireMessages, registered, - systemDisplayname)); + profileKey, systemDisplayName, signalProfileName, + signalProfileAvatar)); } public void setColor(Recipient recipient, MaterialColor color) { @@ -206,6 +229,24 @@ public class RecipientPreferenceDatabase extends Database { updateOrInsert(address, values); } + public void setProfileKey(@NonNull Address address, @Nullable byte[] profileKey) { + ContentValues values = new ContentValues(1); + values.put(PROFILE_KEY, profileKey == null ? null : Base64.encodeBytes(profileKey)); + updateOrInsert(address, values);; + } + + public void setProfileName(@NonNull Address address, @Nullable String profileName) { + ContentValues contentValues = new ContentValues(1); + contentValues.put(SIGNAL_PROFILE_NAME, profileName); + updateOrInsert(address, contentValues); + } + + public void setProfileAvatar(@NonNull Address address, @Nullable String profileAvatar) { + ContentValues contentValues = new ContentValues(1); + contentValues.put(SIGNAL_PROFILE_AVATAR, profileAvatar); + updateOrInsert(address, contentValues); + } + public Set
getAllRecipients() { SQLiteDatabase db = databaseHelper.getReadableDatabase(); Set
results = new HashSet<>(); @@ -285,7 +326,10 @@ public class RecipientPreferenceDatabase extends Database { private final int defaultSubscriptionId; private final int expireMessages; private final boolean registered; + private final byte[] profileKey; private final String systemDisplayName; + private final String signalProfileName; + private final String signalProfileAvatar; RecipientsPreferences(boolean blocked, long muteUntil, @NonNull VibrateState vibrateState, @@ -295,7 +339,10 @@ public class RecipientPreferenceDatabase extends Database { int defaultSubscriptionId, int expireMessages, boolean registered, - String systemDisplayName) + @Nullable byte[] profileKey, + @Nullable String systemDisplayName, + @Nullable String signalProfileName, + @Nullable String signalProfileAvatar) { this.blocked = blocked; this.muteUntil = muteUntil; @@ -306,7 +353,10 @@ public class RecipientPreferenceDatabase extends Database { this.defaultSubscriptionId = defaultSubscriptionId; this.expireMessages = expireMessages; this.registered = registered; + this.profileKey = profileKey; this.systemDisplayName = systemDisplayName; + this.signalProfileName = signalProfileName; + this.signalProfileAvatar = signalProfileAvatar; } public @Nullable MaterialColor getColor() { @@ -345,9 +395,21 @@ public class RecipientPreferenceDatabase extends Database { return registered; } - public String getSystemDisplayName() { + public byte[] getProfileKey() { + return profileKey; + } + + public @Nullable String getSystemDisplayName() { return systemDisplayName; } + + public @Nullable String getProfileName() { + return signalProfileName; + } + + public @Nullable String getProfileAvatar() { + return signalProfileAvatar; + } } public static class BlockedReader { diff --git a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java index e812c801ed..68c9c91260 100644 --- a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java +++ b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java @@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.jobs.PushTextSendJob; import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; import org.thoughtcrime.securesms.jobs.RequestGroupInfoJob; +import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; import org.thoughtcrime.securesms.jobs.RotateSignedPreKeyJob; import org.thoughtcrime.securesms.push.SecurityEventListener; @@ -65,7 +66,8 @@ import dagger.Provides; WebRtcCallService.class, RetrieveProfileJob.class, MultiDeviceVerifiedUpdateJob.class, - CreateProfileActivity.class}) + CreateProfileActivity.class, + RetrieveProfileAvatarJob.class}) public class SignalCommunicationModule { private final Context context; diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 2672f2c590..44e210f3bd 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -25,6 +25,8 @@ import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.PushDatabase; +import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase; +import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.RecipientsPreferences; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.groups.GroupMessageProcessor; @@ -81,6 +83,8 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import java.security.MessageDigest; +import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; @@ -164,6 +168,10 @@ public class PushDecryptJob extends ContextJob { if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false))) { handleUnknownGroupMessage(envelope, message.getGroupInfo().get()); } + + if (message.getProfileKey().isPresent()) { + handleProfileKey(envelope, message); + } } else if (content.getSyncMessage().isPresent()) { SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); @@ -794,6 +802,24 @@ public class PushDecryptJob extends ContextJob { } } + private void handleProfileKey(@NonNull SignalServiceEnvelope envelope, + @NonNull SignalServiceDataMessage message) + { + RecipientPreferenceDatabase database = DatabaseFactory.getRecipientPreferenceDatabase(context); + Address sourceAddress = Address.fromExternal(context, envelope.getSource()); + Optional preferences = database.getRecipientsPreferences(sourceAddress); + + if (!preferences.isPresent() || preferences.get().getProfileKey() == null || + !MessageDigest.isEqual(message.getProfileKey().get(), preferences.get().getProfileKey())) + { + database.setProfileKey(sourceAddress, message.getProfileKey().get()); + + Recipient recipient = RecipientFactory.getRecipientFor(context, sourceAddress, true); + ApplicationContext.getInstance(context).getJobManager().add(new RetrieveProfileJob(context, recipient)); + } + + } + private Optional insertPlaceholder(@NonNull SignalServiceEnvelope envelope) { EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); IncomingTextMessage textMessage = new IncomingTextMessage(Address.fromExternal(context, envelope.getSource()), diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 5fbb4b30c9..5bffd19617 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -162,7 +162,7 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { SignalServiceDataMessage groupMessage = new SignalServiceDataMessage(message.getSentTimeMillis(), group, attachmentStreams, message.getBody(), false, (int)(message.getExpiresIn() / 1000), - message.isExpirationUpdate()); + message.isExpirationUpdate(), null); messageSender.sendMessage(addresses, groupMessage); } diff --git a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java index f9cfa11117..68767b5953 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java @@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException; import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; +import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; @@ -113,11 +114,13 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints(); List scaledAttachments = scaleAttachments(masterSecret, mediaConstraints, message.getAttachments()); List attachmentStreams = getAttachmentsFor(masterSecret, scaledAttachments); + Optional profileKey = getProfileKey(message.getRecipient().getAddress()); SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder() .withBody(message.getBody()) .withAttachments(attachmentStreams) .withTimestamp(message.getSentTimeMillis()) .withExpiration((int)(message.getExpiresIn() / 1000)) + .withProfileKey(profileKey.orNull()) .asExpirationUpdate(message.isExpirationUpdate()) .build(); diff --git a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java index c709155c45..aa2523da76 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.jobs; import android.content.Context; +import android.text.TextUtils; import android.util.Log; import org.greenrobot.eventbus.EventBus; @@ -10,12 +11,16 @@ import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase; +import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.RecipientsPreferences; import org.thoughtcrime.securesms.events.PartProgressEvent; import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.Util; import org.whispersystems.jobqueue.JobParameters; import org.whispersystems.jobqueue.requirements.NetworkRequirement; import org.whispersystems.libsignal.util.guava.Optional; @@ -60,6 +65,28 @@ public abstract class PushSendJob extends SendJob { onPushSend(masterSecret); } + protected Optional getProfileKey(Address address) { + try { + Optional recipientsPreferences = DatabaseFactory.getRecipientPreferenceDatabase(context) + .getRecipientsPreferences(address); + + if (recipientsPreferences.isPresent() && !TextUtils.isEmpty(recipientsPreferences.get().getSystemDisplayName())) { + String profileKey = TextSecurePreferences.getProfileKey(context); + + if (profileKey == null) { + profileKey = Util.getSecret(32); + TextSecurePreferences.setProfileKey(context, profileKey); + } + + return Optional.of(Base64.decode(profileKey)); + } + + return Optional.absent(); + } catch (IOException e) { + throw new AssertionError(e); + } + } + protected SignalServiceAddress getPushAddress(Address address) { // String relay = TextSecureDirectory.getInstance(context).getRelay(address.toPhoneString()); String relay = null; diff --git a/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java index 27c099f663..dfe164f8e4 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java @@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException; import org.thoughtcrime.securesms.transport.RetryLaterException; +import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; @@ -101,10 +102,12 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { try { SignalServiceAddress address = getPushAddress(message.getIndividualRecipient().getAddress()); SignalServiceMessageSender messageSender = messageSenderFactory.create(); + Optional profileKey = getProfileKey(message.getIndividualRecipient().getAddress()); SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder() .withTimestamp(message.getDateSent()) .withBody(message.getBody().getBody()) .withExpiration((int)(message.getExpiresIn() / 1000)) + .withProfileKey(profileKey.orNull()) .asEndSessionMessage(message.isEndSession()) .build(); diff --git a/src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java b/src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java new file mode 100644 index 0000000000..ffa9bfa5d6 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java @@ -0,0 +1,109 @@ +package org.thoughtcrime.securesms.jobs; + + +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; + +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase; +import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.RecipientsPreferences; +import org.thoughtcrime.securesms.dependencies.InjectableType; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientFactory; +import org.thoughtcrime.securesms.util.Util; +import org.whispersystems.jobqueue.JobParameters; +import org.whispersystems.jobqueue.requirements.NetworkRequirement; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; +import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.inject.Inject; + +public class RetrieveProfileAvatarJob extends ContextJob implements InjectableType { + + private static final String TAG = RetrieveProfileAvatarJob.class.getSimpleName(); + + private static final int MAX_PROFILE_SIZE_BYTES = 20 * 1024 * 1024; + + @Inject SignalServiceMessageReceiver receiver; + + private final String profileAvatar; + private final Recipient recipient; + + public RetrieveProfileAvatarJob(Context context, Recipient recipient, String profileAvatar) { + super(context, JobParameters.newBuilder() + .withGroupId(RetrieveProfileAvatarJob.class.getSimpleName() + recipient.getAddress().serialize()) + .withRequirement(new NetworkRequirement(context)) + .create()); + + this.recipient = recipient; + this.profileAvatar = profileAvatar; + } + + @Override + public void onAdded() {} + + @Override + public void onRun() throws IOException { + RecipientPreferenceDatabase database = DatabaseFactory.getRecipientPreferenceDatabase(context); + Optional recipientsPreferences = database.getRecipientsPreferences(recipient.getAddress()); + File avatarDirectory = new File(context.getFilesDir(), "avatars"); + File avatarFile = new File(avatarDirectory, new File(recipient.getAddress().serialize()).getName()); + + avatarDirectory.mkdirs(); + + if (!recipientsPreferences.isPresent()) { + Log.w(TAG, "Recipient preference row is gone!"); + return; + } + + if (recipientsPreferences.get().getProfileKey() == null) { + Log.w(TAG, "Recipient profile key is gone!"); + return; + } + + if (Util.equals(profileAvatar, recipientsPreferences.get().getProfileAvatar())) { + Log.w(TAG, "Already retrieved profile avatar: " + profileAvatar); + return; + } + + if (TextUtils.isEmpty(profileAvatar)) { + Log.w(TAG, "Removing profile avatar for: " + recipient.getAddress().serialize()); + avatarFile.delete(); + return; + } + + File downloadDestination = File.createTempFile("avatar", "jpg", context.getCacheDir()); + + try { + InputStream avatarStream = receiver.retrieveProfileAvatar(profileAvatar, downloadDestination, recipientsPreferences.get().getProfileKey(), MAX_PROFILE_SIZE_BYTES); + File decryptDestination = File.createTempFile("avatar", "jpg", context.getCacheDir()); + + Util.copy(avatarStream, new FileOutputStream(decryptDestination)); + decryptDestination.renameTo(avatarFile); + } finally { + if (downloadDestination != null) downloadDestination.delete(); + } + + database.setProfileAvatar(recipient.getAddress(), profileAvatar); + RecipientFactory.clearCache(context); + } + + @Override + public boolean onShouldRetry(Exception e) { + Log.w(TAG, e); + if (e instanceof PushNetworkException) return true; + return false; + } + + @Override + public void onCanceled() { + + } +} diff --git a/src/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java b/src/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java index dc4c24ae74..952b0511af 100644 --- a/src/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java +++ b/src/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java @@ -6,17 +6,23 @@ import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; +import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.RecipientsPreferences; import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.service.MessageRetrievalService; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.IdentityUtil; +import org.thoughtcrime.securesms.util.Util; import org.whispersystems.jobqueue.JobParameters; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessagePipe; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; +import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.InvalidNumberException; @@ -66,25 +72,13 @@ public class RetrieveProfileJob extends ContextJob implements InjectableType { private void handleIndividualRecipient(Recipient recipient) throws IOException, InvalidKeyException, InvalidNumberException { - String number = recipient.getAddress().toPhoneString(); - SignalServiceProfile profile = retrieveProfile(number); + String number = recipient.getAddress().toPhoneString(); + SignalServiceProfile profile = retrieveProfile(number); + Optional recipientPreferences = DatabaseFactory.getRecipientPreferenceDatabase(context).getRecipientsPreferences(recipient.getAddress()); - if (TextUtils.isEmpty(profile.getIdentityKey())) { - Log.w(TAG, "Identity key is missing on profile!"); - return; - } - - IdentityKey identityKey = new IdentityKey(Base64.decode(profile.getIdentityKey()), 0); - - if (!DatabaseFactory.getIdentityDatabase(context) - .getIdentity(recipient.getAddress()) - .isPresent()) - { - Log.w(TAG, "Still first use..."); - return; - } - - IdentityUtil.saveIdentity(context, number, identityKey); + setIdentityKey(recipient, profile.getIdentityKey()); + setProfileName(recipient, recipientPreferences, profile.getName()); + setProfileAvatar(recipient, recipientPreferences, profile.getAvatar()); } private void handleGroupRecipient(Recipient group) @@ -110,4 +104,59 @@ public class RetrieveProfileJob extends ContextJob implements InjectableType { return receiver.retrieveProfile(new SignalServiceAddress(number)); } + + private void setIdentityKey(Recipient recipient, String identityKeyValue) { + try { + if (TextUtils.isEmpty(identityKeyValue)) { + Log.w(TAG, "Identity key is missing on profile!"); + return; + } + + IdentityKey identityKey = new IdentityKey(Base64.decode(identityKeyValue), 0); + + if (!DatabaseFactory.getIdentityDatabase(context) + .getIdentity(recipient.getAddress()) + .isPresent()) + { + Log.w(TAG, "Still first use..."); + return; + } + + IdentityUtil.saveIdentity(context, recipient.getAddress().toPhoneString(), identityKey); + } catch (InvalidKeyException | IOException e) { + Log.w(TAG, e); + } + } + + private void setProfileName(Recipient recipient, Optional recipientPreferences, String profileName) { + try { + if (!recipientPreferences.isPresent()) return; + if (recipientPreferences.get().getProfileKey() == null) return; + + String plaintextProfileName = null; + + if (profileName != null) { + ProfileCipher profileCipher = new ProfileCipher(recipientPreferences.get().getProfileKey()); + plaintextProfileName = new String(profileCipher.decryptName(Base64.decode(profileName))); + } + + if (!Util.equals(plaintextProfileName, recipientPreferences.get().getProfileName())) { + DatabaseFactory.getRecipientPreferenceDatabase(context).setProfileName(recipient.getAddress(), plaintextProfileName); + RecipientFactory.clearCache(context); + } + } catch (ProfileCipher.InvalidCiphertextException | IOException e) { + Log.w(TAG, e); + } + } + + private void setProfileAvatar(Recipient recipient, Optional recipientPreferences, String profileAvatar) { + if (!recipientPreferences.isPresent()) return; + if (recipientPreferences.get().getProfileKey() == null) return; + + if (!Util.equals(profileAvatar, recipientPreferences.get().getProfileAvatar())) { + ApplicationContext.getInstance(context) + .getJobManager() + .add(new RetrieveProfileAvatarJob(context, recipient, profileAvatar)); + } + } } diff --git a/src/org/thoughtcrime/securesms/mms/TextSecureGlideModule.java b/src/org/thoughtcrime/securesms/mms/TextSecureGlideModule.java index acb59a9e01..9ce5b075da 100644 --- a/src/org/thoughtcrime/securesms/mms/TextSecureGlideModule.java +++ b/src/org/thoughtcrime/securesms/mms/TextSecureGlideModule.java @@ -13,6 +13,8 @@ import org.thoughtcrime.securesms.glide.OkHttpUrlLoader; import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel; import org.thoughtcrime.securesms.mms.ContactPhotoUriLoader.ContactPhotoUri; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; +import org.thoughtcrime.securesms.profiles.AvatarPhotoUriLoader; +import org.thoughtcrime.securesms.profiles.AvatarPhotoUriLoader.AvatarPhotoUri; import java.io.InputStream; @@ -28,6 +30,7 @@ public class TextSecureGlideModule implements GlideModule { glide.register(ContactPhotoUri.class, InputStream.class, new ContactPhotoUriLoader.Factory()); glide.register(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory()); glide.register(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory()); + glide.register(AvatarPhotoUri.class, InputStream.class, new AvatarPhotoUriLoader.Factory()); } public static class NoopDiskCacheFactory implements DiskCache.Factory { diff --git a/src/org/thoughtcrime/securesms/profiles/AvatarPhotoUriFetcher.java b/src/org/thoughtcrime/securesms/profiles/AvatarPhotoUriFetcher.java new file mode 100644 index 0000000000..7dfcdf7d25 --- /dev/null +++ b/src/org/thoughtcrime/securesms/profiles/AvatarPhotoUriFetcher.java @@ -0,0 +1,54 @@ +package org.thoughtcrime.securesms.profiles; + + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.bumptech.glide.Priority; +import com.bumptech.glide.load.data.DataFetcher; + +import org.thoughtcrime.securesms.database.Address; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +public class AvatarPhotoUriFetcher implements DataFetcher { + + private final Context context; + private final Address address; + + private InputStream inputStream; + + public AvatarPhotoUriFetcher(@NonNull Context context, @NonNull Address address) { + this.context = context.getApplicationContext(); + this.address = address; + } + + @Override + public InputStream loadData(Priority priority) throws FileNotFoundException { + File avatarsDir = new File(context.getFilesDir(), "avatars"); + inputStream = new FileInputStream(new File(avatarsDir, address.serialize())); + + return inputStream; + } + + @Override + public void cleanup() { + try { + if (inputStream != null) inputStream.close(); + } catch (IOException e) {} + } + + @Override + public String getId() { + return address.serialize(); + } + + @Override + public void cancel() { + + } +} diff --git a/src/org/thoughtcrime/securesms/profiles/AvatarPhotoUriLoader.java b/src/org/thoughtcrime/securesms/profiles/AvatarPhotoUriLoader.java new file mode 100644 index 0000000000..3b301ddff3 --- /dev/null +++ b/src/org/thoughtcrime/securesms/profiles/AvatarPhotoUriLoader.java @@ -0,0 +1,48 @@ +package org.thoughtcrime.securesms.profiles; + + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.bumptech.glide.load.data.DataFetcher; +import com.bumptech.glide.load.model.GenericLoaderFactory; +import com.bumptech.glide.load.model.ModelLoaderFactory; +import com.bumptech.glide.load.model.stream.StreamModelLoader; + +import org.thoughtcrime.securesms.database.Address; + +import java.io.InputStream; + +public class AvatarPhotoUriLoader implements StreamModelLoader { + + private final Context context; + + public static class Factory implements ModelLoaderFactory { + + @Override + public StreamModelLoader build(Context context, GenericLoaderFactory factories) { + return new AvatarPhotoUriLoader(context); + } + + @Override + public void teardown() {} + } + + public AvatarPhotoUriLoader(Context context) { + this.context = context; + } + + @Override + public DataFetcher getResourceFetcher(AvatarPhotoUri model, int width, int height) { + return new AvatarPhotoUriFetcher(context, model.address); + } + + public static class AvatarPhotoUri { + public @NonNull Address address; + + public AvatarPhotoUri(@NonNull Address address) { + this.address = address; + } + } + +} diff --git a/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java b/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java index 089ff53ec7..5747209e30 100644 --- a/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java +++ b/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java @@ -139,6 +139,7 @@ class RecipientProvider { String name = resultNumber.equals(cursor.getString(0)) ? null : cursor.getString(0); ContactPhoto contactPhoto = ContactPhotoFactory.getContactPhoto(context, Uri.withAppendedPath(Contacts.CONTENT_URI, cursor.getLong(2) + ""), + address, name); return new RecipientDetails(cursor.getString(0), cursor.getString(4), contactUri, contactPhoto, preferences.orNull(), null);