Add basic profile spoofing detection.

This commit is contained in:
Alex Hart
2020-11-04 16:00:12 -04:00
committed by Alan Evans
parent 2f69a9c38e
commit 3dc1614fbc
30 changed files with 1726 additions and 10 deletions

View File

@@ -42,6 +42,7 @@ import android.provider.ContactsContract;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextWatcher;
import android.view.Gravity;
import android.view.KeyEvent;
@@ -68,8 +69,10 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.ViewModelProviders;
@@ -207,6 +210,9 @@ import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewBannerView;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewCardDialogFragment;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment;
@@ -244,8 +250,10 @@ import org.thoughtcrime.securesms.util.MessageUtil;
import org.thoughtcrime.securesms.util.PlayStoreUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SmsUtil;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.TextSecurePreferences.MediaKeyboardMode;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
@@ -342,6 +350,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
protected Stub<ReminderView> reminderView;
private Stub<UnverifiedBannerView> unverifiedBannerView;
private Stub<GroupShareProfileView> groupShareProfileView;
private Stub<ReviewBannerView> reviewBanner;
private TypingStatusTextWatcher typingTextWatcher;
private ConversationSearchBottomBar searchNav;
private MenuItem searchViewItem;
@@ -1829,6 +1838,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
reminderView = ViewUtil.findStubById(this, R.id.reminder_stub);
unverifiedBannerView = ViewUtil.findStubById(this, R.id.unverified_banner_stub);
groupShareProfileView = ViewUtil.findStubById(this, R.id.group_share_profile_view_stub);
reviewBanner = ViewUtil.findStubById(this, R.id.review_banner_stub);
quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle);
inlineAttachmentToggle = ViewUtil.findById(this, R.id.inline_attachment_container);
inputPanel = ViewUtil.findById(this, R.id.bottom_panel);
@@ -1997,6 +2007,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
groupViewModel = ViewModelProviders.of(this, new ConversationGroupViewModel.Factory()).get(ConversationGroupViewModel.class);
recipient.observe(this, groupViewModel::onRecipientChange);
groupViewModel.getGroupActiveState().observe(this, unused -> invalidateOptionsMenu());
groupViewModel.getReviewState().observe(this, this::presentGroupReviewBanner);
}
private void initializeMentionsViewModel() {
@@ -3067,6 +3078,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
messageRequestBottomView.setBlockOnClickListener(v -> onMessageRequestBlockClicked(viewModel));
messageRequestBottomView.setUnblockOnClickListener(v -> onMessageRequestUnblockClicked(viewModel));
viewModel.getRequestReviewDisplayState().observe(this, this::presentRequestReviewBanner);
viewModel.getMessageData().observe(this, this::presentMessageRequestBottomViewTo);
viewModel.getMessageRequestDisplayState().observe(this, this::presentMessageRequestDisplayState);
viewModel.getFailures().observe(this, this::showGroupChangeErrorToast);
@@ -3092,6 +3104,42 @@ public class ConversationActivity extends PassphraseRequiredActivity
});
}
private void presentRequestReviewBanner(@NonNull MessageRequestViewModel.RequestReviewDisplayState state) {
switch (state) {
case SHOWN:
reviewBanner.get().setVisibility(View.VISIBLE);
CharSequence message = new SpannableStringBuilder().append(SpanUtil.bold(getString(R.string.ConversationFragment__review_requests_carefully)))
.append(" ")
.append(getString(R.string.ConversationFragment__signal_found_another_contact_with_the_same_name));
reviewBanner.get().setBannerMessage(message);
Drawable drawable = Objects.requireNonNull(ThemeUtil.getThemedDrawable(this, R.attr.menu_info_icon)).mutate();
DrawableCompat.setTint(drawable, ThemeUtil.getThemedColor(this, R.attr.icon_tint));
reviewBanner.get().setBannerIcon(drawable);
reviewBanner.get().setOnClickListener(unused -> handleReviewRequest(recipient.getId()));
break;
case HIDDEN:
reviewBanner.get().setVisibility(View.GONE);
break;
default:
break;
}
}
private void presentGroupReviewBanner(@NonNull ConversationGroupViewModel.ReviewState groupReviewState) {
if (groupReviewState.getCount() > 0) {
reviewBanner.get().setVisibility(View.VISIBLE);
reviewBanner.get().setBannerMessage(getString(R.string.ConversationFragment__d_group_members_have_the_same_name, groupReviewState.getCount()));
reviewBanner.get().setBannerRecipient(groupReviewState.getRecipient());
reviewBanner.get().setOnClickListener(unused -> handleReviewGroupMembers(groupReviewState.getGroupId()));
} else if (reviewBanner.resolved()) {
reviewBanner.get().setVisibility(View.GONE);
}
}
private void showMessageRequestBusy() {
messageRequestBottomView.showBusy();
}
@@ -3100,6 +3148,24 @@ public class ConversationActivity extends PassphraseRequiredActivity
messageRequestBottomView.hideBusy();
}
private void handleReviewGroupMembers(@Nullable GroupId.V2 groupId) {
if (groupId == null) {
return;
}
ReviewCardDialogFragment.createForReviewMembers(groupId)
.show(getSupportFragmentManager(), null);
}
private void handleReviewRequest(@NonNull RecipientId recipientId) {
if (recipientId == Recipient.UNKNOWN.getId()) {
return;
}
ReviewCardDialogFragment.createForReviewRequest(recipientId)
.show(getSupportFragmentManager(), null);
}
private void showGroupChangeErrorToast(@NonNull GroupChangeFailureReason e) {
Toast.makeText(this, GroupErrors.getUserDisplayMessage(e), Toast.LENGTH_LONG).show();
}

View File

@@ -10,14 +10,19 @@ import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import com.annimon.stream.Stream;
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.GroupChangeBusyException;
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewRecipient;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.thoughtcrime.securesms.util.FeatureFlags;
@@ -25,6 +30,8 @@ import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
final class ConversationGroupViewModel extends ViewModel {
@@ -32,15 +39,31 @@ final class ConversationGroupViewModel extends ViewModel {
private final LiveData<GroupActiveState> groupActiveState;
private final LiveData<GroupDatabase.MemberLevel> selfMembershipLevel;
private final LiveData<Integer> actionableRequestingMembers;
private final LiveData<ReviewState> reviewState;
private ConversationGroupViewModel() {
this.liveRecipient = new MutableLiveData<>();
LiveData<GroupRecord> groupRecord = LiveDataUtil.mapAsync(liveRecipient, ConversationGroupViewModel::getGroupRecordForRecipient);
LiveData<GroupRecord> groupRecord = LiveDataUtil.mapAsync(liveRecipient, ConversationGroupViewModel::getGroupRecordForRecipient);
LiveData<List<Recipient>> duplicates = LiveDataUtil.mapAsync(groupRecord, record -> {
if (record != null && record.isV2Group()) {
return Stream.of(ReviewUtil.getDuplicatedRecipients(record.getId().requireV2()))
.map(ReviewRecipient::getRecipient)
.toList();
} else {
return Collections.emptyList();
}
});
this.groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToGroupActiveState));
this.selfMembershipLevel = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToSelfMembershipLevel));
this.actionableRequestingMembers = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToActionableRequestingMemberCount));
this.reviewState = LiveDataUtil.combineLatest(groupRecord,
duplicates,
(record, dups) -> dups.isEmpty()
? ReviewState.EMPTY
: new ReviewState(record.getId().requireV2(), dups.get(0), dups.size()));
}
void onRecipientChange(Recipient recipient) {
@@ -62,6 +85,10 @@ final class ConversationGroupViewModel extends ViewModel {
return selfMembershipLevel;
}
public LiveData<ReviewState> getReviewState() {
return reviewState;
}
private static @Nullable GroupRecord getGroupRecordForRecipient(@Nullable Recipient recipient) {
if (recipient != null && recipient.isGroup()) {
Application context = ApplicationDependencies.getApplication();
@@ -117,6 +144,33 @@ final class ConversationGroupViewModel extends ViewModel {
});
}
static final class ReviewState {
private static final ReviewState EMPTY = new ReviewState(null, Recipient.UNKNOWN, 0);
private final GroupId.V2 groupId;
private final Recipient recipient;
private final int count;
ReviewState(@Nullable GroupId.V2 groupId, @NonNull Recipient recipient, int count) {
this.groupId = groupId;
this.recipient = recipient;
this.count = count;
}
public @Nullable GroupId.V2 getGroupId() {
return groupId;
}
public @NonNull Recipient getRecipient() {
return recipient;
}
public int getCount() {
return count;
}
}
static final class GroupActiveState {
private final boolean isActive;
private final boolean isActiveV2;