Add support for SN verification

// FREEBIE
master
Moxie Marlinspike 2017-06-06 18:03:09 -07:00
parent 58273997b9
commit 76c28cfa7a
49 changed files with 1532 additions and 250 deletions

View File

@ -61,7 +61,7 @@ dependencies {
compile 'org.whispersystems:jobmanager:1.0.2'
compile 'org.whispersystems:libpastelog:1.0.7'
compile 'org.whispersystems:signal-service-android:2.5.10'
compile 'org.whispersystems:signal-service-android:2.5.11'
compile 'org.whispersystems:webrtc-android:M57-S2'
compile "me.leolin:ShortcutBadger:1.1.16"
@ -135,7 +135,7 @@ dependencyVerification {
'com.google.android.exoplayer:exoplayer:955085aa611a8f7cf6c61b88ae03d1a392f4ad94c9bfbc153f3dedb9ffb14718',
'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181',
'org.whispersystems:libpastelog:bb331d9a98240fc139101128ba836c1edec3c40e000597cdbb29ebf4cbf34d88',
'org.whispersystems:signal-service-android:0c6937decce77b94807d100a16bcffc9e69489057b4430ca07a5bce618637b6e',
'org.whispersystems:signal-service-android:355c2b139a7587bbde899261a0344bc6a32c1cf0baa5ac748f6c86190c07374c',
'org.whispersystems:webrtc-android:9d11e39d4b3823713e5b1486226e0ce09f989d6f47f52da1815e406c186701d5',
'me.leolin:ShortcutBadger:e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774',
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
@ -169,7 +169,7 @@ dependencyVerification {
'com.google.android.gms:play-services-base:0ca636a8fc9a5af45e607cdcd61783bf5d561cbbb0f862021ce69606eee5ad49',
'com.google.android.gms:play-services-basement:95dd882c5ffba15b9a99de3fefb05d3a01946623af67454ca00055d222f85a8d',
'com.google.android.gms:play-services-iid:54e919f9957b8b7820da7ee9b83471d00d0cac1cf08ddea8b5b41aea80bb1a70',
'org.whispersystems:signal-service-java:1de66a4068523098951529a363b4f02436e654d3d3fcf9228154b8c2cf94945b',
'org.whispersystems:signal-service-java:02956dc0c22d8a6c7cf613b2e40cf54aec15a5e1cc5acab4d0f8b82bc22f2e0d',
'org.whispersystems:signal-protocol-android:b05cd9570d2e262afeb6610b70f473a936c54dd23a7c967d76e8f288766731fd',
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

View File

@ -23,6 +23,12 @@
android:clipToPadding="false"
android:clipChildren="false">
<ViewStub android:id="@+id/unverified_banner_stub"
android:layout="@layout/conversation_activity_unverified_banner_stub"
android:inflatedId="@+id/unverified_banner"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<ViewStub
android:id="@+id/reminder_stub"
android:layout="@layout/conversation_activity_reminderview_stub"

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.identity.UnverifiedBannerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/unverified_banner"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

View File

@ -21,14 +21,31 @@
style="@style/TextSecure.TitleTextStyle"
tools:ignore="UnusedAttribute"/>
<TextView android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:layout_gravity="center_vertical|start"
android:gravity="center_vertical"
android:textDirection="ltr"
style="@style/TextSecure.SubtitleTextStyle"/>
<LinearLayout android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<ImageView android:id="@+id/verified_indicator"
android:src="@drawable/ic_check_circle_white_18dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="3dp"
android:layout_gravity="bottom"
android:alpha="0.7"
android:visibility="gone"/>
<TextView android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:layout_gravity="center_vertical|start"
android:gravity="center_vertical"
android:textDirection="ltr"
tools:text="(123) 123-1234"
style="@style/TextSecure.SubtitleTextStyle"/>
</LinearLayout>
</org.thoughtcrime.securesms.ConversationTitleView>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/reminder_background"
android:focusable="true"
android:nextFocusRight="@+id/cancel"
android:orientation="horizontal"
tools:visibility="visible">
<ImageView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_alert"/>
<TextView android:id="@+id/unverified_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="16sp"
android:layout_weight="1"
android:layout_margin="16dp"
tools:text="Your safety number with Jules Bonnot has changed and is no longer verified"/>
<ImageButton
android:id="@+id/cancel"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginTop="2dp"
android:background="?selectableItemBackgroundBorderless"
android:focusable="true"
android:nextFocusLeft="@+id/container"
android:nextFocusRight="@+id/container"
android:src="@drawable/ic_close_white_24dp"
android:contentDescription="@string/InviteActivity_cancel"/>
</LinearLayout>

View File

@ -12,17 +12,25 @@
android:background="?verification_background"
android:orientation="vertical">
<FrameLayout android:layout_width="wrap_content"
android:layout_height="wrap_content">
<FrameLayout android:layout_width="250dp"
android:layout_height="250dp">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="20sp"
android:text="@string/verify_display_fragment__loading"/>
<org.thoughtcrime.securesms.components.SquareImageView
android:id="@+id/qr_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="20dp"
android:padding="20dp"
android:background="@drawable/qr_code_background"
tools:src="@drawable/splash_logo"/>
android:visibility="invisible"
tools:src="@drawable/splash_logo"
tools:visibility="invisible"/>
<TextView android:id="@+id/tap_label"
android:layout_width="wrap_content"
@ -31,6 +39,7 @@
android:layout_marginBottom="35dp"
android:textColor="@color/gray50"
android:textSize="11sp"
android:visibility="invisible"
android:text="@string/verify_display_fragment__tap_to_scan"/>
<org.thoughtcrime.securesms.components.SquareImageView
@ -146,10 +155,30 @@
</TableRow>
</TableLayout>
<LinearLayout android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:paddingLeft="20dp">
<android.support.v7.widget.SwitchCompat
android:id="@+id/verified_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_marginLeft="5dp"
android:textSize="17dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/verify_display_fragment__verified"/>
</LinearLayout>
<TextView android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="25dp"
android:layout_marginTop="15dp"
android:textSize="17sp"
android:lineSpacingExtra="3sp"
android:text="@string/verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s"/>

View File

@ -421,6 +421,10 @@
<string name="MessageRecord_you_set_disappearing_message_time_to_s">You set disappearing message time to %1$s.</string>
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s set disappearing message time to %2$s.</string>
<string name="MessageRecord_your_safety_number_with_s_has_changed">Your safety number with %s has changed.</string>
<string name="MessageRecord_you_marked_your_safety_number_with_s_verified">You marked your safety number with %s verified</string>
<string name="MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device">You marked your safety number with %s verified from another device</string>
<string name="MessageRecord_you_marked_your_safety_number_with_s_unverified">You marked your safety number with %s unverified</string>
<string name="MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device">You marked your safety number with %s unverified from another device</string>
<!-- PassphraseChangeActivity -->
@ -594,6 +598,8 @@
<string name="ThreadRecord_disappearing_message_time_updated_to_s">Disappearing message time set to %s</string>
<string name="ThreadRecord_safety_number_changed">Safety number changed</string>
<string name="ThreadRecord_your_safety_number_with_s_has_changed">Your safety number with %s has changed.</string>
<string name="ThreadRecord_you_marked_verified">You marked verified</string>
<string name="ThreadRecord_you_marked_unverified">You marked unverified</string>
<!-- UpdateApkReadyListener -->
<string name="UpdateApkReadyListener_Signal_update">Signal update</string>
@ -851,6 +857,24 @@
</plurals>
<string name="expiration_weeks_abbreviated">%dw</string>
<!-- unverified safety numbers -->
<string name="IdentityUtil_unverified_banner_one">Your safety number with %s has changed and is no longer verified</string>
<string name="IdentityUtil_unverified_banner_two">Your safety numbers with %1$s and %2$s are no longer verified</string>
<string name="IdentityUtil_unverified_banner_many">Your safety numbers with %1$s, %2$s, and %3$s are no longer verified</string>
<string name="IdentityUtil_unverified_dialog_one">Your safety number with %1$s has changed and is no longer verified. This could either mean that someone is trying to intercept your communication, or that %1$s simply reinstalled Signal.</string>
<string name="IdentityUtil_unverified_dialog_two">Your safety numbers with %1$s and %2$s are no longer verified. This could either mean that someone is trying to intercept your communication, or that they simply reinstalled Signal.</string>
<string name="IdentityUtil_unverified_dialog_many">Your safety numbers with %1$s, %2$s, and %3$s are no longer verified. This could either mean that someone is trying to intercept your communication, or that they simply reinstalled Signal.</string>
<string name="IdentityUtil_untrusted_dialog_one">Your safety number with %s just changed.</string>
<string name="IdentityUtil_untrusted_dialog_two">Your safety number with %1$s and %2$s just changed.</string>
<string name="IdentityUtil_untrusted_dialog_many">Your safety number with %1$s, %2$s, and %3$s just changed.</string>
<plurals name="identity_others">
<item quantity="one">%d other</item>
<item quantity="other">%d others</item>
</plurals>
<!-- giphy_activity -->
<string name="giphy_activity_toolbar__search_gifs_and_stickers">Search GIFs and stickers</string>
@ -1022,8 +1046,10 @@
<string name="recipients_panel__add_members">Add members</string>
<!-- verify_display_fragment -->
<string name="verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s"><![CDATA[If you wish to verify the security of your end-to-end encryption with %s, compare the number above with the number on their device. Alternatively, you can scan the code on their phone, or ask them to scan your code. <a href="https://whispersystems.org/redirect/safety-numbers">Learn more about verifying safety numbers</a>]]></string>
<string name="verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s"><![CDATA[If you wish to verify the security of your encryption with %s, compare the number above with the number on their device. Alternatively, you can scan the code on their phone, or ask them to scan your code. <a href="https://whispersystems.org/redirect/safety-numbers">Learn more.</a>]]></string>
<string name="verify_display_fragment__tap_to_scan">Tap to scan</string>
<string name="verify_display_fragment__loading">Loading...</string>
<string name="verify_display_fragment__verified">Verified</string>
<!-- verify_identity -->
<string name="verify_identity__share_safety_number">Share safety number</string>
@ -1375,6 +1401,10 @@
<!-- transport_selection_list_item -->
<string name="transport_selection_list_item__transport_icon">Transport icon</string>
<string name="UntrustedSendDialog_send_message">Send message?</string>
<string name="UntrustedSendDialog_send">Send</string>
<string name="UnverifiedSendDialog_send_message">Send message?</string>
<string name="UnverifiedSendDialog_send">Send</string>
<!-- EOF -->

View File

@ -110,7 +110,7 @@ public class ConfirmIdentityDialog extends AlertDialog {
SignalProtocolAddress mismatchAddress = new SignalProtocolAddress(number, 1);
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(getContext());
identityKeyStore.saveIdentity(mismatchAddress, mismatch.getIdentityKey(), true, true);
identityKeyStore.saveIdentity(mismatchAddress, mismatch.getIdentityKey(), true);
}
processMessageRecord(messageRecord);

View File

@ -85,11 +85,15 @@ import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer;
import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer.AttachmentDrawerListener;
import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer.DrawerState;
import org.thoughtcrime.securesms.components.emoji.EmojiDrawer;
import org.thoughtcrime.securesms.components.identity.UntrustedSendDialog;
import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView;
import org.thoughtcrime.securesms.components.identity.UnverifiedSendDialog;
import org.thoughtcrime.securesms.components.location.SignalPlace;
import org.thoughtcrime.securesms.components.reminder.InviteReminder;
import org.thoughtcrime.securesms.components.reminder.ReminderView;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
@ -98,12 +102,16 @@ import org.thoughtcrime.securesms.database.DraftDatabase;
import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
import org.thoughtcrime.securesms.database.DraftDatabase.Drafts;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.RecipientPreferenceEvent;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.RecipientsPreferences;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.identity.IdentityRecordList;
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.mms.AttachmentManager;
@ -141,6 +149,7 @@ import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
@ -155,11 +164,13 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import static org.thoughtcrime.securesms.TransportOption.Type;
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
/**
@ -199,19 +210,20 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private static final int PICK_GIF = 9;
private static final int SMS_DEFAULT = 10;
private MasterSecret masterSecret;
protected ComposeText composeText;
private AnimatingToggle buttonToggle;
private SendButton sendButton;
private ImageButton attachButton;
protected ConversationTitleView titleView;
private TextView charactersLeft;
private ConversationFragment fragment;
private Button unblockButton;
private Button makeDefaultSmsButton;
private InputAwareLayout container;
private View composePanel;
protected Stub<ReminderView> reminderView;
private MasterSecret masterSecret;
protected ComposeText composeText;
private AnimatingToggle buttonToggle;
private SendButton sendButton;
private ImageButton attachButton;
protected ConversationTitleView titleView;
private TextView charactersLeft;
private ConversationFragment fragment;
private Button unblockButton;
private Button makeDefaultSmsButton;
private InputAwareLayout container;
private View composePanel;
protected Stub<ReminderView> reminderView;
private Stub<UnverifiedBannerView> unverifiedBannerView;
private AttachmentTypeSelector attachmentTypeSelector;
private AttachmentManager attachmentManager;
@ -231,8 +243,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private boolean isDefaultSms = true;
private boolean isMmsEnabled = true;
private DynamicTheme dynamicTheme = new DynamicTheme();
private DynamicLanguage dynamicLanguage = new DynamicLanguage();
private final IdentityRecordList identityRecords = new IdentityRecordList();
private final DynamicTheme dynamicTheme = new DynamicTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
@Override
protected void onPreCreate() {
@ -308,6 +321,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
initializeEnabledCheck();
initializeMmsEnabledCheck();
initializeIdentityRecords();
composeText.setTransport(sendButton.getSelectedTransport());
titleView.setTitle(recipients);
@ -317,7 +331,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
MessageNotifier.setVisibleThread(threadId);
markThreadAsRead();
markIdentitySeen();
Log.w(TAG, "onResume() Finished: " + (System.currentTimeMillis() - getIntent().getLongExtra(TIMING_EXTRA, 0)));
}
@ -854,6 +867,56 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
startActivity(intent);
}
private void handleUnverifiedRecipients() {
List<Recipient> unverifiedRecipients = identityRecords.getUnverifiedRecipients(this);
List<IdentityRecord> unverifiedRecords = identityRecords.getUnverifiedRecords();
String message = IdentityUtil.getUnverifiedSendDialogDescription(this, unverifiedRecipients);
if (message == null) return;
new UnverifiedSendDialog(this, message, unverifiedRecords, new UnverifiedSendDialog.ResendListener() {
@Override
public void onResendMessage() {
initializeIdentityRecords().addListener(new ListenableFuture.Listener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
sendMessage();
}
@Override
public void onFailure(ExecutionException e) {
throw new AssertionError(e);
}
});
}
}).show();
}
private void handleUntrustedRecipients() {
List<Recipient> untrustedRecipients = identityRecords.getUntrustedRecipients(this);
List<IdentityRecord> untrustedRecords = identityRecords.getUntrustedRecords();
String untrustedMessage = IdentityUtil.getUntrustedSendDialogDescription(this, untrustedRecipients);
if (untrustedMessage == null) return;
new UntrustedSendDialog(this, untrustedMessage, untrustedRecords, new UntrustedSendDialog.ResendListener() {
@Override
public void onResendMessage() {
initializeIdentityRecords().addListener(new ListenableFuture.Listener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
sendMessage();
}
@Override
public void onFailure(ExecutionException e) {
throw new AssertionError(e);
}
});
}
}).show();
}
private void handleSecurityChange(boolean isSecureText, boolean isDefaultSms) {
this.isSecureText = isSecureText;
this.isDefaultSms = isDefaultSms;
@ -1027,6 +1090,64 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}.execute();
}
private ListenableFuture<Boolean> initializeIdentityRecords() {
final SettableFuture<Boolean> future = new SettableFuture<>();
new AsyncTask<Recipients, Void, Pair<IdentityRecordList, String>>() {
@Override
protected @NonNull Pair<IdentityRecordList, String> doInBackground(Recipients... params) {
try {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(ConversationActivity.this);
IdentityRecordList identityRecordList = new IdentityRecordList();
Recipients recipients = params[0];
if (recipients.isGroupRecipient()) {
recipients = DatabaseFactory.getGroupDatabase(ConversationActivity.this)
.getGroupMembers(GroupUtil.getDecodedId(recipients.getPrimaryRecipient().getNumber()), false);
}
for (long recipientId : recipients.getIds()) {
Log.w(TAG, "Loading identity for: " + recipientId);
identityRecordList.add(identityDatabase.getIdentity(recipientId));
}
String message = null;
if (identityRecordList.isUnverified()) {
message = IdentityUtil.getUnverifiedBannerDescription(ConversationActivity.this, identityRecordList.getUnverifiedRecipients(ConversationActivity.this));
}
return new Pair<>(identityRecordList, message);
} catch (IOException e) {
throw new AssertionError(e);
}
}
@Override
protected void onPostExecute(@NonNull Pair<IdentityRecordList, String> result) {
Log.w(TAG, "Got identity records: " + result.first.isUnverified());
identityRecords.replaceWith(result.first);
if (result.second != null) {
Log.w(TAG, "Replacing banner...");
unverifiedBannerView.get().display(result.second, result.first.getUnverifiedRecords(),
new UnverifiedClickedListener(),
new UnverifiedDismissedListener());
} else if (unverifiedBannerView.resolved()) {
Log.w(TAG, "Clearing banner...");
unverifiedBannerView.get().hide();
}
titleView.setVerified(identityRecords.isVerified());
future.set(true);
}
}.execute(recipients);
return future;
}
private void initializeViews() {
titleView = (ConversationTitleView) getSupportActionBar().getCustomView();
buttonToggle = ViewUtil.findById(this, R.id.button_toggle);
@ -1040,6 +1161,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
composePanel = ViewUtil.findById(this, R.id.bottom_panel);
container = ViewUtil.findById(this, R.id.layout_container);
reminderView = ViewUtil.findStubById(this, R.id.reminder_stub);
unverifiedBannerView = ViewUtil.findStubById(this, R.id.unverified_banner_stub);
quickAttachmentDrawer = ViewUtil.findById(this, R.id.quick_attachment_drawer);
quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle);
inputPanel = ViewUtil.findById(this, R.id.bottom_panel);
@ -1157,6 +1279,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
public void run() {
titleView.setTitle(recipients);
titleView.setVerified(identityRecords.isVerified());
setBlockedUserState(recipients, isSecureText, isDefaultSms);
setActionBarColor(recipients.getColor());
invalidateOptionsMenu();
@ -1172,6 +1295,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onIdentityRecordUpdate(final IdentityRecord event) {
initializeIdentityRecords();
}
private void initializeReceivers() {
securityUpdateReceiver = new BroadcastReceiver() {
@Override
@ -1443,17 +1571,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}.execute(threadId);
}
private void markIdentitySeen() {
new AsyncTask<Recipient, Void, Void>() {
@Override
protected Void doInBackground(Recipient... params) {
DatabaseFactory.getIdentityDatabase(ConversationActivity.this)
.setSeen(params[0].getRecipientId());
return null;
}
}.execute(recipients.getPrimaryRecipient());
}
protected void sendComplete(long threadId) {
boolean refreshFragment = (threadId != this.threadId);
this.threadId = threadId;
@ -1490,6 +1607,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if ((!recipients.isSingleRecipient() || recipients.isEmailRecipient()) && !isMmsEnabled) {
handleManualMmsRequired();
} else if (!forceSms && identityRecords.isUnverified()) {
handleUnverifiedRecipients();
} else if (!forceSms && identityRecords.isUntrusted()) {
handleUntrustedRecipients();
} else if (attachmentManager.isAttachmentPresent() || !recipients.isSingleRecipient() || recipients.isGroupRecipient() || recipients.isEmailRecipient()) {
sendMediaMessage(forceSms, expiresIn, subscriptionId);
} else {
@ -1886,4 +2007,68 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
}
private class UnverifiedDismissedListener implements UnverifiedBannerView.DismissListener {
@Override
public void onDismissed(final List<IdentityRecord> unverifiedIdentities) {
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(ConversationActivity.this);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
synchronized (SESSION_LOCK) {
for (IdentityRecord identityRecord : unverifiedIdentities) {
identityDatabase.setVerified(identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
VerifiedStatus.DEFAULT);
}
}
return null;
}
@Override
protected void onPostExecute(Void result) {
initializeIdentityRecords();
}
}.execute();
}
}
private class UnverifiedClickedListener implements UnverifiedBannerView.ClickListener {
@Override
public void onClicked(final List<IdentityRecord> unverifiedIdentities) {
Log.w(TAG, "onClicked: " + unverifiedIdentities.size());
if (unverifiedIdentities.size() == 1) {
Intent intent = new Intent(ConversationActivity.this, VerifyIdentityActivity.class);
intent.putExtra(VerifyIdentityActivity.RECIPIENT_ID_EXTRA, unverifiedIdentities.get(0).getRecipientId());
intent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(unverifiedIdentities.get(0).getIdentityKey()));
intent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, false);
startActivity(intent);
} else {
String[] unverifiedNames = new String[unverifiedIdentities.size()];
for (int i=0;i<unverifiedIdentities.size();i++) {
unverifiedNames[i] = RecipientFactory.getRecipientForId(ConversationActivity.this, unverifiedIdentities.get(i).getRecipientId(), false).toShortString();
}
AlertDialog.Builder builder = new AlertDialog.Builder(ConversationActivity.this);
builder.setIconAttribute(R.attr.dialog_alert_icon);
builder.setTitle("No longer verified");
builder.setItems(unverifiedNames, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent(ConversationActivity.this, VerifyIdentityActivity.class);
intent.putExtra(VerifyIdentityActivity.RECIPIENT_ID_EXTRA, unverifiedIdentities.get(which).getRecipientId());
intent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(unverifiedIdentities.get(which).getIdentityKey()));
intent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, false);
startActivity(intent);
}
});
builder.show();
}
}
}
}

View File

@ -240,7 +240,10 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
@Override
public int getItemViewType(@NonNull MessageRecord messageRecord) {
if (messageRecord.isGroupAction() || messageRecord.isCallLog() || messageRecord.isJoined() ||
messageRecord.isExpirationTimerUpdate() || messageRecord.isEndSession() || messageRecord.isIdentityUpdate()) {
messageRecord.isExpirationTimerUpdate() || messageRecord.isEndSession() ||
messageRecord.isIdentityUpdate() || messageRecord.isIdentityVerified() ||
messageRecord.isIdentityDefault())
{
return MESSAGE_TYPE_UPDATE;
} else if (hasAudio(messageRecord)) {
if (messageRecord.isOutgoing()) return MESSAGE_TYPE_AUDIO_OUTGOING;

View File

@ -223,7 +223,8 @@ public class ConversationFragment extends Fragment
for (MessageRecord messageRecord : messageRecords) {
if (messageRecord.isGroupAction() || messageRecord.isCallLog() ||
messageRecord.isJoined() || messageRecord.isExpirationTimerUpdate() ||
messageRecord.isEndSession() || messageRecord.isIdentityUpdate())
messageRecord.isEndSession() || messageRecord.isIdentityUpdate() ||
messageRecord.isIdentityVerified() || messageRecord.isIdentityDefault())
{
actionMessage = true;
break;

View File

@ -5,6 +5,7 @@ import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
@ -18,6 +19,7 @@ public class ConversationTitleView extends LinearLayout {
private TextView title;
private TextView subtitle;
private ImageView verified;
public ConversationTitleView(Context context) {
this(context, null);
@ -32,8 +34,9 @@ public class ConversationTitleView extends LinearLayout {
public void onFinishInflate() {
super.onFinishInflate();
this.title = (TextView) findViewById(R.id.title);
this.subtitle = (TextView) findViewById(R.id.subtitle);
this.title = (TextView) findViewById(R.id.title);
this.subtitle = (TextView) findViewById(R.id.subtitle);
this.verified = (ImageView) findViewById(R.id.verified_indicator);
ViewUtil.setTextViewGravityStart(this.title, getContext());
ViewUtil.setTextViewGravityStart(this.subtitle, getContext());
@ -53,6 +56,10 @@ public class ConversationTitleView extends LinearLayout {
}
}
public void setVerified(boolean verified) {
this.verified.setVisibility(verified ? View.VISIBLE : View.GONE);
}
private void setComposeTitle() {
this.title.setText(R.string.ConversationActivity_compose_message);
this.subtitle.setText(null);

View File

@ -16,6 +16,8 @@ import android.widget.TextView;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
@ -24,7 +26,6 @@ import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Locale;
@ -96,6 +97,8 @@ public class ConversationUpdateItem extends LinearLayout
else if (messageRecord.isExpirationTimerUpdate()) setTimerRecord(messageRecord);
else if (messageRecord.isEndSession()) setEndSessionRecord(messageRecord);
else if (messageRecord.isIdentityUpdate()) setIdentityRecord(messageRecord);
else if (messageRecord.isIdentityVerified() ||
messageRecord.isIdentityDefault()) setIdentityVerifyUpdate(messageRecord);
else throw new AssertionError("Neither group nor log nor joined.");
if (batchSelected.contains(messageRecord)) setSelected(true);
@ -132,6 +135,15 @@ public class ConversationUpdateItem extends LinearLayout
date.setVisibility(View.GONE);
}
private void setIdentityVerifyUpdate(final MessageRecord messageRecord) {
if (messageRecord.isIdentityVerified()) icon.setImageResource(R.drawable.ic_check_white_24dp);
else icon.setImageResource(R.drawable.ic_info_outline_white_24dp);
icon.setColorFilter(new PorterDuffColorFilter(Color.parseColor("#757575"), PorterDuff.Mode.MULTIPLY));
body.setText(messageRecord.getDisplayBody());
date.setVisibility(View.GONE);
}
private void setGroupRecord(MessageRecord messageRecord) {
icon.setImageResource(R.drawable.ic_group_grey600_24dp);
icon.clearColorFilter();
@ -193,20 +205,25 @@ public class ConversationUpdateItem extends LinearLayout
@Override
public void onClick(View v) {
if (!messageRecord.isIdentityUpdate() || !batchSelected.isEmpty()) {
if ((!messageRecord.isIdentityUpdate() &&
!messageRecord.isIdentityDefault() &&
!messageRecord.isIdentityVerified()) ||
!batchSelected.isEmpty())
{
if (parent != null) parent.onClick(v);
return;
}
final Recipient sender = ConversationUpdateItem.this.sender;
IdentityUtil.getRemoteIdentityKey(getContext(), masterSecret, sender).addListener(new ListenableFuture.Listener<Optional<IdentityKey>>() {
IdentityUtil.getRemoteIdentityKey(getContext(), sender).addListener(new ListenableFuture.Listener<Optional<IdentityRecord>>() {
@Override
public void onSuccess(Optional<IdentityKey> result) {
public void onSuccess(Optional<IdentityRecord> result) {
if (result.isPresent()) {
Intent intent = new Intent(getContext(), VerifyIdentityActivity.class);
intent.putExtra(VerifyIdentityActivity.RECIPIENT_ID, sender.getRecipientId());
intent.putExtra(VerifyIdentityActivity.RECIPIENT_IDENTITY, new IdentityKeyParcelable(result.get()));
intent.putExtra(VerifyIdentityActivity.RECIPIENT_ID_EXTRA, sender.getRecipientId());
intent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(result.get().getIdentityKey()));
intent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, result.get().getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED);
getContext().startActivity(intent);
}

View File

@ -21,6 +21,7 @@ import android.support.v4.app.Fragment;
import android.support.v4.preference.PreferenceFragment;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
@ -32,6 +33,8 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.VibrateState;
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
@ -302,9 +305,9 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
if (recipients.isBlocked()) blockPreference.setTitle(R.string.RecipientPreferenceActivity_unblock);
else blockPreference.setTitle(R.string.RecipientPreferenceActivity_block);
IdentityUtil.getRemoteIdentityKey(getActivity(), masterSecret, recipients.getPrimaryRecipient()).addListener(new ListenableFuture.Listener<Optional<IdentityKey>>() {
IdentityUtil.getRemoteIdentityKey(getActivity(), recipients.getPrimaryRecipient()).addListener(new ListenableFuture.Listener<Optional<IdentityRecord>>() {
@Override
public void onSuccess(Optional<IdentityKey> result) {
public void onSuccess(Optional<IdentityRecord> result) {
if (result.isPresent()) {
if (identityPreference != null) identityPreference.setOnPreferenceClickListener(new IdentityClickedListener(result.get()));
if (identityPreference != null) identityPreference.setEnabled(true);
@ -458,17 +461,19 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
private class IdentityClickedListener implements Preference.OnPreferenceClickListener {
private final IdentityKey identityKey;
private final IdentityRecord identityKey;
private IdentityClickedListener(IdentityKey identityKey) {
private IdentityClickedListener(IdentityRecord identityKey) {
Log.w(TAG, "Identity record: " + identityKey);
this.identityKey = identityKey;
}
@Override
public boolean onPreferenceClick(Preference preference) {
Intent verifyIdentityIntent = new Intent(getActivity(), VerifyIdentityActivity.class);
verifyIdentityIntent.putExtra(VerifyIdentityActivity.RECIPIENT_ID, recipients.getPrimaryRecipient().getRecipientId());
verifyIdentityIntent.putExtra(VerifyIdentityActivity.RECIPIENT_IDENTITY, new IdentityKeyParcelable(identityKey));
verifyIdentityIntent.putExtra(VerifyIdentityActivity.RECIPIENT_ID_EXTRA, recipients.getPrimaryRecipient().getRecipientId());
verifyIdentityIntent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(identityKey.getIdentityKey()));
verifyIdentityIntent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, identityKey.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED);
startActivity(verifyIdentityIntent);
return true;

View File

@ -16,6 +16,8 @@
*/
package org.thoughtcrime.securesms;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
@ -26,13 +28,18 @@ import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Vibrator;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
import android.support.v4.animation.AnimatorCompatHelper;
import android.support.v4.animation.ValueAnimatorCompat;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.widget.SwitchCompat;
import android.text.Html;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
@ -49,7 +56,7 @@ import android.view.animation.Animation;
import android.view.animation.AnticipateInterpolator;
import android.view.animation.OvershootInterpolator;
import android.view.animation.ScaleAnimation;
import android.widget.AdapterView;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
@ -59,14 +66,18 @@ import org.thoughtcrime.securesms.components.camera.CameraView;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUnion;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
import org.thoughtcrime.securesms.qr.QrCode;
import org.thoughtcrime.securesms.qr.ScanListener;
import org.thoughtcrime.securesms.qr.ScanningThread;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
@ -80,6 +91,8 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
/**
* Activity for verifying identity keys.
*
@ -89,8 +102,9 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
private static final String TAG = VerifyIdentityActivity.class.getSimpleName();
public static final String RECIPIENT_ID = "recipient_id";
public static final String RECIPIENT_IDENTITY = "recipient_identity";
public static final String RECIPIENT_ID_EXTRA = "recipient_id";
public static final String IDENTITY_EXTRA = "recipient_identity";
public static final String VERIFIED_EXTRA = "verified_state";
private final DynamicTheme dynamicTheme = new DynamicTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
@ -110,16 +124,18 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.AndroidManifest__verify_safety_number);
Recipient recipient = RecipientFactory.getRecipientForId(this, getIntent().getLongExtra(RECIPIENT_ID, -1), true);
Recipient recipient = RecipientFactory.getRecipientForId(this, getIntent().getLongExtra(RECIPIENT_ID_EXTRA, -1), true);
recipient.addListener(this);
setActionBarNotificationBarColor(recipient.getColor());
Bundle extras = new Bundle();
extras.putParcelable(VerifyDisplayFragment.REMOTE_IDENTITY, getIntent().getParcelableExtra(RECIPIENT_IDENTITY));
extras.putLong(VerifyDisplayFragment.REMOTE_RECIPIENT_ID, getIntent().getLongExtra(RECIPIENT_ID_EXTRA, -1));
extras.putParcelable(VerifyDisplayFragment.REMOTE_IDENTITY, getIntent().getParcelableExtra(IDENTITY_EXTRA));
extras.putString(VerifyDisplayFragment.REMOTE_NUMBER, Util.canonicalizeNumber(this, recipient.getNumber()));
extras.putParcelable(VerifyDisplayFragment.LOCAL_IDENTITY, new IdentityKeyParcelable(IdentityKeyUtil.getIdentityKey(this)));
extras.putString(VerifyDisplayFragment.LOCAL_NUMBER, TextSecurePreferences.getLocalNumber(this));
extras.putBoolean(VerifyDisplayFragment.VERIFIED_STATE, getIntent().getBooleanExtra(VERIFIED_EXTRA, false));
scanFragment.setScanListener(this);
displayFragment.setClickListener(this);
@ -212,16 +228,19 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
}
public static class VerifyDisplayFragment extends Fragment implements Recipients.RecipientsModifiedListener {
public static class VerifyDisplayFragment extends Fragment implements Recipient.RecipientModifiedListener, CompoundButton.OnCheckedChangeListener {
public static final String REMOTE_NUMBER = "remote_number";
public static final String REMOTE_IDENTITY = "remote_identity";
public static final String LOCAL_IDENTITY = "local_identity";
public static final String LOCAL_NUMBER = "local_number";
public static final String REMOTE_RECIPIENT_ID = "remote_recipient_id";
public static final String REMOTE_NUMBER = "remote_number";
public static final String REMOTE_IDENTITY = "remote_identity";
public static final String LOCAL_IDENTITY = "local_identity";
public static final String LOCAL_NUMBER = "local_number";
public static final String VERIFIED_STATE = "verified_state";
private Recipients recipient;
private String localNumber;
private String remoteNumber;
private MasterSecret masterSecret;
private Recipient recipient;
private String localNumber;
private String remoteNumber;
private IdentityKey localIdentity;
private IdentityKey remoteIdentity;
@ -232,8 +251,10 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
private View numbersContainer;
private ImageView qrCode;
private ImageView qrVerified;
private TextView tapLabel;
private TextView description;
private View.OnClickListener clickListener;
private SwitchCompat verified;
private TextView[] codes = new TextView[12];
private boolean animateSuccessOnDraw = false;
@ -244,8 +265,10 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_display_fragment);
this.numbersContainer = ViewUtil.findById(container, R.id.number_table);
this.qrCode = ViewUtil.findById(container, R.id.qr_code);
this.verified = ViewUtil.findById(container, R.id.verified_switch);
this.qrVerified = ViewUtil.findById(container, R.id.qr_verified);
this.description = ViewUtil.findById(container, R.id.description);
this.tapLabel = ViewUtil.findById(container, R.id.tap_label);
this.codes[0] = ViewUtil.findById(container, R.id.code_first);
this.codes[1] = ViewUtil.findById(container, R.id.code_second);
this.codes[2] = ViewUtil.findById(container, R.id.code_third);
@ -262,6 +285,9 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
this.qrCode.setOnClickListener(clickListener);
this.registerForContextMenu(numbersContainer);
this.verified.setChecked(getArguments().getBoolean(VERIFIED_STATE, false));
this.verified.setOnCheckedChangeListener(this);
return container;
}
@ -269,23 +295,37 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
this.masterSecret = getArguments().getParcelable("master_secret");
this.localNumber = getArguments().getString(LOCAL_NUMBER);
this.localIdentity = ((IdentityKeyParcelable)getArguments().getParcelable(LOCAL_IDENTITY)).get();
this.remoteNumber = getArguments().getString(REMOTE_NUMBER);
this.recipient = RecipientFactory.getRecipientsFromString(getActivity(), this.remoteNumber, true);
this.recipient = RecipientFactory.getRecipientForId(getActivity(), getArguments().getLong(REMOTE_RECIPIENT_ID), true);
this.remoteIdentity = ((IdentityKeyParcelable)getArguments().getParcelable(REMOTE_IDENTITY)).get();
this.fingerprint = new NumericFingerprintGenerator(5200).createFor(localNumber, localIdentity,
remoteNumber, remoteIdentity);
this.recipient.addListener(this);
new AsyncTask<Void, Void, Fingerprint>() {
@Override
protected Fingerprint doInBackground(Void... params) {
return new NumericFingerprintGenerator(5200).createFor(localNumber, localIdentity,
remoteNumber, remoteIdentity);
}
@Override
protected void onPostExecute(Fingerprint fingerprint) {
VerifyDisplayFragment.this.fingerprint = fingerprint;
setFingerprintViews(fingerprint, true);
}
}.execute();
}
@Override
public void onModified(Recipients recipients) {
public void onModified(final Recipient recipient) {
Util.runOnMain(new Runnable() {
@Override
public void run() {
setFingerprintViews(fingerprint);
setRecipientText(recipient);
}
});
}
@ -294,7 +334,11 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
public void onResume() {
super.onResume();
setFingerprintViews(fingerprint);
setRecipientText(recipient);
if (fingerprint != null) {
setFingerprintViews(fingerprint, false);
}
if (animateSuccessOnDraw) {
animateSuccessOnDraw = false;
@ -398,12 +442,19 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
}
}
private void setFingerprintViews(Fingerprint fingerprint) {
private void setRecipientText(Recipient recipient) {
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.toShortString())));
description.setMovementMethod(LinkMovementMethod.getInstance());
}
private void setFingerprintViews(Fingerprint fingerprint, boolean animate) {
String digits = fingerprint.getDisplayableFingerprint().getDisplayText();
int partSize = digits.length() / codes.length;
for (int i=0;i<codes.length;i++) {
codes[i].setText(digits.substring(i * partSize, (i * partSize) + partSize));
String substring = digits.substring(i * partSize, (i * partSize) + partSize);
if (animate) setCodeSegment(codes[i], substring);
else codes[i].setText(substring);
}
byte[] qrCodeData = fingerprint.getScannableFingerprint().getSerialized();
@ -411,8 +462,41 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
Bitmap qrCodeBitmap = QrCode.create(qrCodeString);
qrCode.setImageBitmap(qrCodeBitmap);
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.toShortString())));
description.setMovementMethod(LinkMovementMethod.getInstance());
if (animate) {
ViewUtil.fadeIn(qrCode, 1000);
ViewUtil.fadeIn(tapLabel, 1000);
} else {
qrCode.setVisibility(View.VISIBLE);
tapLabel.setVisibility(View.VISIBLE);
}
}
private void setCodeSegment(final TextView codeView, String segment) {
if (Build.VERSION.SDK_INT >= 11) {
ValueAnimator valueAnimator = new ValueAnimator();
valueAnimator.setObjectValues(0, Integer.parseInt(segment));
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
codeView.setText(String.format("%05d", value));
}
});
valueAnimator.setEvaluator(new TypeEvaluator<Integer>() {
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
return Math.round(startValue + (endValue - startValue) * fraction);
}
});
valueAnimator.setDuration(1000);
valueAnimator.start();
} else {
codeView.setText(segment);
}
}
private Bitmap createVerifiedBitmap(int width, int height, @DrawableRes int id) {
@ -478,6 +562,41 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
ViewUtil.animateIn(qrVerified, scaleAnimation);
}
@Override
public void onCheckedChanged(CompoundButton buttonView, final boolean isChecked) {
new AsyncTask<Recipient, Void, Void>() {
@Override
protected Void doInBackground(Recipient... params) {
synchronized (SESSION_LOCK) {
if (isChecked) {
Log.w(TAG, "Saving identity: " + params[0].getRecipientId());
DatabaseFactory.getIdentityDatabase(getActivity())
.saveIdentity(params[0].getRecipientId(),
remoteIdentity,
VerifiedStatus.VERIFIED, false,
System.currentTimeMillis(), true);
} else {
DatabaseFactory.getIdentityDatabase(getActivity())
.setVerified(params[0].getRecipientId(),
remoteIdentity,
VerifiedStatus.DEFAULT);
}
ApplicationContext.getInstance(getActivity())
.getJobManager()
.add(new MultiDeviceVerifiedUpdateJob(getActivity(),
recipient.getNumber(),
remoteIdentity,
isChecked ? VerifiedStatus.VERIFIED :
VerifiedStatus.DEFAULT));
IdentityUtil.markIdentityVerified(getActivity(), new MasterSecretUnion(masterSecret), recipient, isChecked, false);
}
return null;
}
}.execute(recipient);
}
}
public static class VerifyScanFragment extends Fragment {

View File

@ -258,7 +258,7 @@ public class WebRtcCallActivity extends Activity {
public void onClick(View v) {
synchronized (SESSION_LOCK) {
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(WebRtcCallActivity.this);
identityKeyStore.saveIdentity(new SignalProtocolAddress(recipient.getNumber(), 1), theirIdentity, true, true);
identityKeyStore.saveIdentity(new SignalProtocolAddress(recipient.getNumber(), 1), theirIdentity, true);
}
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);

View File

@ -0,0 +1,66 @@
package org.thoughtcrime.securesms.components.identity;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import java.util.List;
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
public class UntrustedSendDialog extends AlertDialog.Builder implements DialogInterface.OnClickListener {
private final List<IdentityRecord> untrustedRecords;
private final ResendListener resendListener;
public UntrustedSendDialog(@NonNull Context context,
@NonNull String message,
@NonNull List<IdentityRecord> untrustedRecords,
@NonNull ResendListener resendListener)
{
super(context);
this.untrustedRecords = untrustedRecords;
this.resendListener = resendListener;
setTitle(R.string.UntrustedSendDialog_send_message);
setIconAttribute(R.attr.dialog_alert_icon);
setMessage(message);
setPositiveButton(R.string.UntrustedSendDialog_send, this);
setNegativeButton(android.R.string.cancel, null);
}
@Override
public void onClick(DialogInterface dialog, int which) {
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
synchronized (SESSION_LOCK) {
for (IdentityRecord identityRecord : untrustedRecords) {
identityDatabase.setApproval(identityRecord.getRecipientId(), true);
}
}
return null;
}
@Override
protected void onPostExecute(Void result) {
resendListener.onResendMessage();
}
}.execute();
}
public interface ResendListener {
public void onResendMessage();
}
}

View File

@ -0,0 +1,98 @@
package org.thoughtcrime.securesms.components.identity;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.List;
public class UnverifiedBannerView extends LinearLayout {
private static final String TAG = UnverifiedBannerView.class.getSimpleName();
private View container;
private TextView text;
private ImageView closeButton;
public UnverifiedBannerView(Context context) {
super(context);
initialize();
}
public UnverifiedBannerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initialize();
}
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
public UnverifiedBannerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public UnverifiedBannerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initialize();
}
private void initialize() {
LayoutInflater.from(getContext()).inflate(R.layout.unverified_banner_view, this, true);
this.container = ViewUtil.findById(this, R.id.container);
this.text = ViewUtil.findById(this, R.id.unverified_text);
this.closeButton = ViewUtil.findById(this, R.id.cancel);
}
public void display(@NonNull final String text,
@NonNull final List<IdentityRecord> unverifiedIdentities,
@NonNull final ClickListener clickListener,
@NonNull final DismissListener dismissListener)
{
this.text.setText(text);
setVisibility(View.VISIBLE);
this.container.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.w(TAG, "onClick()");
clickListener.onClicked(unverifiedIdentities);
}
});
this.closeButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
hide();
dismissListener.onDismissed(unverifiedIdentities);
}
});
}
public void hide() {
setVisibility(View.GONE);
}
public interface DismissListener {
public void onDismissed(List<IdentityRecord> unverifiedIdentities);
}
public interface ClickListener {
public void onClicked(List<IdentityRecord> unverifiedIdentities);
}
}

View File

@ -0,0 +1,67 @@
package org.thoughtcrime.securesms.components.identity;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import java.util.List;
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
public class UnverifiedSendDialog extends AlertDialog.Builder implements DialogInterface.OnClickListener {
private final List<IdentityRecord> untrustedRecords;
private final ResendListener resendListener;
public UnverifiedSendDialog(@NonNull Context context,
@NonNull String message,
@NonNull List<IdentityRecord> untrustedRecords,
@NonNull ResendListener resendListener)
{
super(context);
this.untrustedRecords = untrustedRecords;
this.resendListener = resendListener;
setTitle(R.string.UnverifiedSendDialog_send_message);
setIconAttribute(R.attr.dialog_alert_icon);
setMessage(message);
setPositiveButton(R.string.UnverifiedSendDialog_send, this);
setNegativeButton(android.R.string.cancel, null);
}
@Override
public void onClick(DialogInterface dialog, int which) {
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
synchronized (SESSION_LOCK) {
for (IdentityRecord identityRecord : untrustedRecords) {
identityDatabase.setVerified(identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
IdentityDatabase.VerifiedStatus.DEFAULT);
}
}
return null;
}
@Override
protected void onPostExecute(Void result) {
resendListener.onResendMessage();
}
}.execute();
}
public interface ResendListener {
public void onResendMessage();
}
}

View File

@ -6,9 +6,12 @@ import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.List;
public class SessionUtil {
public static boolean hasSession(Context context, MasterSecret masterSecret, Recipient recipient) {
@ -21,4 +24,23 @@ public class SessionUtil {
return sessionStore.containsSession(axolotlAddress);
}
public static void archiveSiblingSessions(Context context, SignalProtocolAddress address) {
SessionStore sessionStore = new TextSecureSessionStore(context);
List<Integer> devices = sessionStore.getSubDeviceSessions(address.getName());
devices.add(1);
for (int device : devices) {
if (device != address.getDeviceId()) {
SignalProtocolAddress sibling = new SignalProtocolAddress(address.getName(), device);
if (sessionStore.containsSession(sibling)) {
SessionRecord sessionRecord = sessionStore.loadSession(sibling);
sessionRecord.archiveCurrentState();
sessionStore.storeSession(sibling, sessionRecord);
}
}
}
}
}

View File

@ -4,9 +4,11 @@ import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.IdentityUtil;
@ -42,9 +44,7 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
return TextSecurePreferences.getLocalRegistrationId(context);
}
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey,
boolean blockingApproval, boolean nonBlockingApproval)
{
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey, boolean nonBlockingApproval) {
synchronized (LOCK) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, address.getName(), true);
@ -53,20 +53,29 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
if (!identityRecord.isPresent()) {
Log.w(TAG, "Saving new identity...");
identityDatabase.saveIdentity(recipientId, identityKey, true, System.currentTimeMillis(), blockingApproval, nonBlockingApproval);
identityDatabase.saveIdentity(recipientId, identityKey, VerifiedStatus.DEFAULT, true, System.currentTimeMillis(), nonBlockingApproval);
return false;
}
if (!identityRecord.get().getIdentityKey().equals(identityKey)) {
Log.w(TAG, "Replacing existing identity...");
identityDatabase.saveIdentity(recipientId, identityKey, false, System.currentTimeMillis(), blockingApproval, nonBlockingApproval);
VerifiedStatus verifiedStatus;
if (identityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED) {
verifiedStatus = VerifiedStatus.UNVERIFIED;
} else {
verifiedStatus = VerifiedStatus.DEFAULT;
}
identityDatabase.saveIdentity(recipientId, identityKey, verifiedStatus, false, System.currentTimeMillis(), nonBlockingApproval);
IdentityUtil.markIdentityUpdate(context, recipients.getPrimaryRecipient());
SessionUtil.archiveSiblingSessions(context, address);
return true;
}
if (isBlockingApprovalRequired(identityRecord.get()) || isNonBlockingApprovalRequired(identityRecord.get())) {
if (isNonBlockingApprovalRequired(identityRecord.get())) {
Log.w(TAG, "Setting approval status...");
identityDatabase.setApproval(recipientId, blockingApproval, nonBlockingApproval);
identityDatabase.setApproval(recipientId, nonBlockingApproval);
return false;
}
@ -76,7 +85,7 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
@Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return saveIdentity(address, identityKey, !TextSecurePreferences.isSendingIdentityApprovalRequired(context), false);
return saveIdentity(address, identityKey, false);
}
@Override
@ -110,8 +119,8 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
return false;
}
if (isBlockingApprovalRequired(identityRecord.get())) {
Log.w(TAG, "Needs blocking approval!");
if (identityRecord.get().getVerifiedStatus() == VerifiedStatus.UNVERIFIED) {
Log.w(TAG, "Needs unverified approval!");
return false;
}
@ -123,12 +132,6 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
return true;
}
private boolean isBlockingApprovalRequired(IdentityRecord identityRecord) {
return !identityRecord.isFirstUse() &&
TextSecurePreferences.isSendingIdentityApprovalRequired(context) &&
!identityRecord.isApprovedBlocking();
}
private boolean isNonBlockingApprovalRequired(IdentityRecord identityRecord) {
return !identityRecord.isFirstUse() &&
System.currentTimeMillis() - identityRecord.getTimestamp() < TimeUnit.SECONDS.toMillis(TIMESTAMP_THRESHOLD_SECONDS) &&

View File

@ -871,14 +871,11 @@ public class DatabaseFactory {
if (oldVersion < INTRODUCED_IDENTITY_TIMESTAMP) {
db.execSQL("ALTER TABLE identities ADD COLUMN timestamp INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE identities ADD COLUMN first_use INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE identities ADD COLUMN seen INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE identities ADD COLUMN blocking_approval INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE identities ADD COLUMN nonblocking_approval INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE identities ADD COLuMN verified INTEGER DEFAULT 0");
db.execSQL("DROP INDEX archived_index");
db.execSQL("CREATE INDEX IF NOT EXISTS archived_count_index ON thread (archived, message_count)");
db.execSQL("UPDATE identities SET blocking_approval = '1'");
}
db.setTransactionSuccessful();

View File

@ -21,9 +21,10 @@ import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.util.Log;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.greenrobot.eventbus.EventBus;
import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
@ -35,17 +36,14 @@ public class IdentityDatabase extends Database {
private static final String TAG = IdentityDatabase.class.getSimpleName();
private static final Uri CHANGE_URI = Uri.parse("content://textsecure/identities");
private static final String TABLE_NAME = "identities";
private static final String ID = "_id";
private static final String RECIPIENT = "recipient";
private static final String IDENTITY_KEY = "key";
private static final String TIMESTAMP = "timestamp";
private static final String FIRST_USE = "first_use";
private static final String SEEN = "seen";
private static final String BLOCKING_APPROVAL = "blocking_approval";
private static final String NONBLOCKING_APPROVAL = "nonblocking_approval";
private static final String VERIFIED = "verified";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME +
" (" + ID + " INTEGER PRIMARY KEY, " +
@ -53,14 +51,41 @@ public class IdentityDatabase extends Database {
IDENTITY_KEY + " TEXT, " +
FIRST_USE + " INTEGER DEFAULT 0, " +
TIMESTAMP + " INTEGER DEFAULT 0, " +
SEEN + " INTEGER DEFAULT 0, " +
BLOCKING_APPROVAL + " INTEGER DEFAULT 0, " +
VERIFIED + " INTEGER DEFAULT 0, " +
NONBLOCKING_APPROVAL + " INTEGER DEFAULT 0);";
public enum VerifiedStatus {
DEFAULT, VERIFIED, UNVERIFIED;
public int toInt() {
if (this == DEFAULT) return 0;
else if (this == VERIFIED) return 1;
else if (this == UNVERIFIED) return 2;
else throw new AssertionError();
}
public static VerifiedStatus forState(int state) {
if (state == 0) return DEFAULT;
else if (state == 1) return VERIFIED;
else if (state == 2) return UNVERIFIED;
else throw new AssertionError("No such state: " + state);
}
}
IdentityDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
public Cursor getIdentities() {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
return database.query(TABLE_NAME, null, null, null, null, null, null);
}
public @Nullable IdentityReader readerFor(@Nullable Cursor cursor) {
if (cursor == null) return null;
return new IdentityReader(cursor);
}
public Optional<IdentityRecord> getIdentity(long recipientId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null;
@ -70,15 +95,7 @@ public class IdentityDatabase extends Database {
new String[] {recipientId + ""}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
String serializedIdentity = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP));
long seen = cursor.getLong(cursor.getColumnIndexOrThrow(SEEN));
boolean blockingApproval = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCKING_APPROVAL)) == 1;
boolean nonblockingApproval = cursor.getInt(cursor.getColumnIndexOrThrow(NONBLOCKING_APPROVAL)) == 1;
boolean firstUse = cursor.getInt(cursor.getColumnIndexOrThrow(FIRST_USE)) == 1;
IdentityKey identity = new IdentityKey(Base64.decode(serializedIdentity), 0);
return Optional.of(new IdentityRecord(identity, firstUse, timestamp, seen, blockingApproval, nonblockingApproval));
return Optional.of(getIdentityRecord(cursor));
}
} catch (InvalidKeyException | IOException e) {
throw new AssertionError(e);
@ -89,8 +106,8 @@ public class IdentityDatabase extends Database {
return Optional.absent();
}
public void saveIdentity(long recipientId, IdentityKey identityKey, boolean firstUse,
long timestamp, boolean blockingApproval, boolean nonBlockingApproval)
public void saveIdentity(long recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus,
boolean firstUse, long timestamp, boolean nonBlockingApproval)
{
SQLiteDatabase database = databaseHelper.getWritableDatabase();
String identityKeyString = Base64.encodeBytes(identityKey.serialize());
@ -99,60 +116,74 @@ public class IdentityDatabase extends Database {
contentValues.put(RECIPIENT, recipientId);
contentValues.put(IDENTITY_KEY, identityKeyString);
contentValues.put(TIMESTAMP, timestamp);
contentValues.put(BLOCKING_APPROVAL, blockingApproval ? 1 : 0);
contentValues.put(VERIFIED, verifiedStatus.toInt());
contentValues.put(NONBLOCKING_APPROVAL, nonBlockingApproval ? 1 : 0);
contentValues.put(FIRST_USE, firstUse ? 1 : 0);
contentValues.put(SEEN, 0);
database.replace(TABLE_NAME, null, contentValues);
context.getContentResolver().notifyChange(CHANGE_URI, null);
EventBus.getDefault().post(new IdentityRecord(recipientId, identityKey, verifiedStatus,
firstUse, timestamp, nonBlockingApproval));
}
public void setApproval(long recipientId, boolean blockingApproval, boolean nonBlockingApproval) {
public void setApproval(long recipientId, boolean nonBlockingApproval) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues(2);
contentValues.put(BLOCKING_APPROVAL, blockingApproval);
contentValues.put(NONBLOCKING_APPROVAL, nonBlockingApproval);
database.update(TABLE_NAME, contentValues, RECIPIENT + " = ?",
new String[] {String.valueOf(recipientId)});
context.getContentResolver().notifyChange(CHANGE_URI, null);
}
public void setSeen(long recipientId) {
Log.w(TAG, "Setting seen to current time: " + recipientId);
public void setVerified(long recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues(1);
contentValues.put(SEEN, System.currentTimeMillis());
contentValues.put(VERIFIED, verifiedStatus.toInt());
database.update(TABLE_NAME, contentValues, RECIPIENT + " = ? AND " + SEEN + " = 0",
new String[] {String.valueOf(recipientId)});
database.update(TABLE_NAME, contentValues, RECIPIENT + " = ? AND " + IDENTITY_KEY + " = ?",
new String[] {String.valueOf(recipientId),
Base64.encodeBytes(identityKey.serialize())});
}
private IdentityRecord getIdentityRecord(@NonNull Cursor cursor) throws IOException, InvalidKeyException {
long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT));
String serializedIdentity = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP));
int verifiedStatus = cursor.getInt(cursor.getColumnIndexOrThrow(VERIFIED));
boolean nonblockingApproval = cursor.getInt(cursor.getColumnIndexOrThrow(NONBLOCKING_APPROVAL)) == 1;
boolean firstUse = cursor.getInt(cursor.getColumnIndexOrThrow(FIRST_USE)) == 1;
IdentityKey identity = new IdentityKey(Base64.decode(serializedIdentity), 0);
return new IdentityRecord(recipientId, identity, VerifiedStatus.forState(verifiedStatus), firstUse, timestamp, nonblockingApproval);
}
public static class IdentityRecord {
private final IdentityKey identitykey;
private final boolean firstUse;
private final long timestamp;
private final long seen;
private final boolean blockingApproval;
private final boolean nonblockingApproval;
private final long recipientId;
private final IdentityKey identitykey;
private final VerifiedStatus verifiedStatus;
private final boolean firstUse;
private final long timestamp;
private final boolean nonblockingApproval;
private IdentityRecord(IdentityKey identitykey, boolean firstUse, long timestamp,
long seen, boolean blockingApproval, boolean nonblockingApproval)
private IdentityRecord(long recipientId,
IdentityKey identitykey, VerifiedStatus verifiedStatus,
boolean firstUse, long timestamp, boolean nonblockingApproval)
{
this.recipientId = recipientId;
this.identitykey = identitykey;
this.verifiedStatus = verifiedStatus;
this.firstUse = firstUse;
this.timestamp = timestamp;
this.seen = seen;
this.blockingApproval = blockingApproval;
this.nonblockingApproval = nonblockingApproval;
}
public long getRecipientId() {
return recipientId;
}
public IdentityKey getIdentityKey() {
return identitykey;
}
@ -161,12 +192,8 @@ public class IdentityDatabase extends Database {
return timestamp;
}
public long getSeen() {
return seen;
}
public boolean isApprovedBlocking() {
return blockingApproval;
public VerifiedStatus getVerifiedStatus() {
return verifiedStatus;
}
public boolean isApprovedNonBlocking() {
@ -177,6 +204,35 @@ public class IdentityDatabase extends Database {
return firstUse;
}
@Override
public String toString() {
return "{recipientId: " + recipientId + ", identityKey: " + identitykey + ", verifiedStatus: " + verifiedStatus + ", firstUse: " + firstUse + "}";
}
}
public class IdentityReader {
private final Cursor cursor;
public IdentityReader(@NonNull Cursor cursor) {
this.cursor = cursor;
}
public @Nullable IdentityRecord getNext() {
if (cursor.moveToNext()) {
try {
return getIdentityRecord(cursor);
} catch (IOException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
return null;
}
public void close() {
cursor.close();
}
}
}

View File

@ -50,15 +50,15 @@ public interface MmsSmsColumns {
protected static final long MESSAGE_FORCE_SMS_BIT = 0x40;
// Key Exchange Information
protected static final long KEY_EXCHANGE_MASK = 0xFF00;
protected static final long KEY_EXCHANGE_BIT = 0x8000;
protected static final long KEY_EXCHANGE_STALE_BIT = 0x4000;
protected static final long KEY_EXCHANGE_PROCESSED_BIT = 0x2000;
protected static final long KEY_EXCHANGE_CORRUPTED_BIT = 0x1000;
protected static final long KEY_EXCHANGE_INVALID_VERSION_BIT = 0x800;
protected static final long KEY_EXCHANGE_BUNDLE_BIT = 0x400;
protected static final long KEY_EXCHANGE_IDENTITY_UPDATE_BIT = 0x200;
protected static final long KEY_EXCHANGE_CONTENT_FORMAT = 0x100;
protected static final long KEY_EXCHANGE_MASK = 0xFF00;
protected static final long KEY_EXCHANGE_BIT = 0x8000;
protected static final long KEY_EXCHANGE_IDENTITY_VERIFIED_BIT = 0x4000;
protected static final long KEY_EXCHANGE_IDENTITY_DEFAULT_BIT = 0x2000;
protected static final long KEY_EXCHANGE_CORRUPTED_BIT = 0x1000;
protected static final long KEY_EXCHANGE_INVALID_VERSION_BIT = 0x800;
protected static final long KEY_EXCHANGE_BUNDLE_BIT = 0x400;
protected static final long KEY_EXCHANGE_IDENTITY_UPDATE_BIT = 0x200;
protected static final long KEY_EXCHANGE_CONTENT_FORMAT = 0x100;
// Secure Message Information
protected static final long SECURE_MESSAGE_BIT = 0x800000;
@ -112,7 +112,7 @@ public interface MmsSmsColumns {
public static boolean isPendingMessageType(long type) {
return
(type & BASE_TYPE_MASK) == BASE_OUTBOX_TYPE ||
(type & BASE_TYPE_MASK) == BASE_SENDING_TYPE;
(type & BASE_TYPE_MASK) == BASE_SENDING_TYPE;
}
public static boolean isPendingSmsFallbackType(long type) {
@ -152,12 +152,12 @@ public interface MmsSmsColumns {
return (type & KEY_EXCHANGE_BIT) != 0;
}
public static boolean isStaleKeyExchange(long type) {
return (type & KEY_EXCHANGE_STALE_BIT) != 0;
public static boolean isIdentityVerified(long type) {
return (type & KEY_EXCHANGE_IDENTITY_VERIFIED_BIT) != 0;
}
public static boolean isProcessedKeyExchange(long type) {
return (type & KEY_EXCHANGE_PROCESSED_BIT) != 0;
public static boolean isIdentityDefault(long type) {
return (type & KEY_EXCHANGE_IDENTITY_DEFAULT_BIT) != 0;
}
public static boolean isCorruptedKeyExchange(long type) {

View File

@ -521,6 +521,9 @@ public class SmsDatabase extends MessagingDatabase {
if (message.isIdentityUpdate()) type |= Types.KEY_EXCHANGE_IDENTITY_UPDATE_BIT;
if (message.isContentPreKeyBundle()) type |= Types.KEY_EXCHANGE_CONTENT_FORMAT;
if (message.isIdentityVerified()) type |= Types.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT;
else if (message.isIdentityDefault()) type |= Types.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT;
Recipients recipients;
if (message.getSender() != null) {
@ -540,7 +543,7 @@ public class SmsDatabase extends MessagingDatabase {
boolean unread = (org.thoughtcrime.securesms.util.Util.isDefaultSmsProvider(context) ||
message.isSecureMessage() || message.isGroup() || message.isPreKeyBundle()) &&
!message.isIdentityUpdate();
!message.isIdentityUpdate() && !message.isIdentityDefault() && !message.isIdentityVerified();
long threadId;
@ -577,7 +580,7 @@ public class SmsDatabase extends MessagingDatabase {
DatabaseFactory.getThreadDatabase(context).setUnread(threadId);
}
if (!message.isIdentityUpdate()) {
if (!message.isIdentityUpdate() && !message.isIdentityVerified() && !message.isIdentityDefault()) {
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
}
@ -605,6 +608,9 @@ public class SmsDatabase extends MessagingDatabase {
else if (message.isEndSession()) type |= Types.END_SESSION_BIT;
if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT;
if (message.isIdentityVerified()) type |= Types.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT;
else if (message.isIdentityDefault()) type |= Types.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT;
String address = message.getRecipients().getPrimaryRecipient().getNumber();
ContentValues contentValues = new ContentValues(6);

View File

@ -0,0 +1,116 @@
package org.thoughtcrime.securesms.database.identity;
import android.content.Context;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class IdentityRecordList {
private static final String TAG = IdentityRecordList.class.getSimpleName();
private final List<IdentityRecord> identityRecords = new LinkedList<>();
public void add(Optional<IdentityRecord> identityRecord) {
if (identityRecord.isPresent()) {
identityRecords.add(identityRecord.get());
}
}
public void replaceWith(IdentityRecordList identityRecordList) {
identityRecords.clear();
identityRecords.addAll(identityRecordList.identityRecords);
}
public boolean isVerified() {
for (IdentityRecord identityRecord : identityRecords) {
if (identityRecord.getVerifiedStatus() != VerifiedStatus.VERIFIED) {
return false;
}
}
return identityRecords.size() > 0;
}
public boolean isUnverified() {
for (IdentityRecord identityRecord : identityRecords) {
if (identityRecord.getVerifiedStatus() == VerifiedStatus.UNVERIFIED) {
return true;
}
}
return false;
}
public boolean isUntrusted() {
for (IdentityRecord identityRecord : identityRecords) {
if (isUntrusted(identityRecord)) {
return true;
}
}
return false;
}
public List<IdentityRecord> getUntrustedRecords() {
List<IdentityRecord> results = new LinkedList<>();
for (IdentityRecord identityRecord : identityRecords) {
if (isUntrusted(identityRecord)) {
results.add(identityRecord);
}
}
return results;
}
public List<Recipient> getUntrustedRecipients(Context context) {
List<Recipient> untrusted = new LinkedList<>();
for (IdentityRecord identityRecord : identityRecords) {
if (isUntrusted(identityRecord)) {
untrusted.add(RecipientFactory.getRecipientForId(context, identityRecord.getRecipientId(), false));
}
}
return untrusted;
}
public List<IdentityRecord> getUnverifiedRecords() {
List<IdentityRecord> results = new LinkedList<>();
for (IdentityRecord identityRecord : identityRecords) {
if (identityRecord.getVerifiedStatus() == VerifiedStatus.UNVERIFIED) {
results.add(identityRecord);
}
}
return results;
}
public List<Recipient> getUnverifiedRecipients(Context context) {
List<Recipient> unverified = new LinkedList<>();
for (IdentityRecord identityRecord : identityRecords) {
if (identityRecord.getVerifiedStatus() == VerifiedStatus.UNVERIFIED) {
unverified.add(RecipientFactory.getRecipientForId(context, identityRecord.getRecipientId(), false));
}
}
return unverified;
}
private boolean isUntrusted(IdentityRecord identityRecord) {
return !identityRecord.isApprovedNonBlocking() &&
System.currentTimeMillis() - identityRecord.getTimestamp() < TimeUnit.SECONDS.toMillis(5);
}
}

View File

@ -70,7 +70,9 @@ public abstract class DisplayRecord {
}
public boolean isPending() {
return MmsSmsColumns.Types.isPendingMessageType(type);
return MmsSmsColumns.Types.isPendingMessageType(type) &&
!MmsSmsColumns.Types.isIdentityVerified(type) &&
!MmsSmsColumns.Types.isIdentityDefault(type);
}
public boolean isOutgoing() {

View File

@ -114,6 +114,12 @@ public abstract class MessageRecord extends DisplayRecord {
: emphasisAdded(context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, getIndividualRecipient().toShortString(), time));
} else if (isIdentityUpdate()) {
return emphasisAdded(context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, getIndividualRecipient().toShortString()));
} else if (isIdentityVerified()) {
if (isOutgoing()) return emphasisAdded(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified, getIndividualRecipient().toShortString()));
else return emphasisAdded(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device, getIndividualRecipient().toShortString()));
} else if (isIdentityDefault()) {
if (isOutgoing()) return emphasisAdded(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, getIndividualRecipient().toShortString()));
else return emphasisAdded(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, getIndividualRecipient().toShortString()));
} else if (getBody().getBody().length() > MAX_DISPLAY_LENGTH) {
return new SpannableString(getBody().getBody().substring(0, MAX_DISPLAY_LENGTH));
}
@ -140,12 +146,12 @@ public abstract class MessageRecord extends DisplayRecord {
return SmsDatabase.Types.isForcedSms(type);
}
public boolean isStaleKeyExchange() {
return SmsDatabase.Types.isStaleKeyExchange(type);
public boolean isIdentityVerified() {
return SmsDatabase.Types.isIdentityVerified(type);
}
public boolean isProcessedKeyExchange() {
return SmsDatabase.Types.isProcessedKeyExchange(type);
public boolean isIdentityDefault() {
return SmsDatabase.Types.isIdentityDefault(type);
}
public boolean isIdentityMismatchFailure() {

View File

@ -64,10 +64,6 @@ public class SmsMessageRecord extends MessageRecord {
public SpannableString getDisplayBody() {
if (SmsDatabase.Types.isFailedDecryptType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
} else if (isProcessedKeyExchange()) {
return new SpannableString("");
} else if (isStaleKeyExchange()) {
return emphasisAdded(context.getString(R.string.ConversationItem_error_received_stale_key_exchange_message));
} else if (isCorruptedKeyExchange()) {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_received_corrupted_key_exchange_message));
} else if (isInvalidVersionKeyExchange()) {

View File

@ -105,6 +105,10 @@ public class ThreadRecord extends DisplayRecord {
} else if (SmsDatabase.Types.isIdentityUpdate(type)) {
if (getRecipients().isGroupRecipient()) return emphasisAdded(context.getString(R.string.ThreadRecord_safety_number_changed));
else return emphasisAdded(context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, getRecipients().getPrimaryRecipient().toShortString()));
} else if (SmsDatabase.Types.isIdentityVerified(type)) {
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified));
} else if (SmsDatabase.Types.isIdentityDefault(type)) {
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_unverified));
} else {
if (TextUtils.isEmpty(getBody().getBody())) {
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message)));

View File

@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob;
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
@ -61,7 +62,8 @@ import dagger.Provides;
AvatarDownloadJob.class,
RotateSignedPreKeyJob.class,
WebRtcCallService.class,
RetrieveProfileJob.class})
RetrieveProfileJob.class,
MultiDeviceVerifiedUpdateJob.class})
public class SignalCommunicationModule {
private final Context context;

View File

@ -0,0 +1,153 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
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.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import javax.inject.Inject;
public class MultiDeviceVerifiedUpdateJob extends ContextJob implements InjectableType {
private static final long serialVersionUID = 1L;
private static final String TAG = MultiDeviceVerifiedUpdateJob.class.getSimpleName();
@Inject
transient SignalCommunicationModule.SignalMessageSenderFactory messageSenderFactory;
private final String destination;
private final byte[] identityKey;
private final VerifiedStatus verifiedStatus;
public MultiDeviceVerifiedUpdateJob(Context context, String destination, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
super(context, JobParameters.newBuilder()
.withRequirement(new NetworkRequirement(context))
.withPersistence()
.withGroupId("__MULTI_DEVICE_VERIFIED_UPDATE__")
.create());
this.destination = destination;
this.identityKey = identityKey.serialize();
this.verifiedStatus = verifiedStatus;
}
public MultiDeviceVerifiedUpdateJob(Context context) {
super(context, JobParameters.newBuilder()
.withRequirement(new NetworkRequirement(context))
.withPersistence()
.withGroupId("__MULTI_DEVICE_VERIFIED_UPDATE__")
.create());
this.destination = null;
this.identityKey = null;
this.verifiedStatus = null;
}
@Override
public void onRun() throws IOException, UntrustedIdentityException {
try {
if (!TextSecurePreferences.isMultiDevice(context)) {
Log.w(TAG, "Not multi device...");
return;
}
if (destination != null) sendSpecificUpdate(destination, identityKey, verifiedStatus);
else sendFullUpdate();
} catch (InvalidNumberException | InvalidKeyException e) {
throw new IOException(e);
}
}
private void sendSpecificUpdate(String destination, byte[] identityKey, VerifiedStatus verifiedStatus)
throws IOException, UntrustedIdentityException, InvalidNumberException, InvalidKeyException
{
String canonicalDestination = Util.canonicalizeNumber(context, destination);
VerifiedMessage.VerifiedState verifiedState = getVerifiedState(verifiedStatus);
SignalServiceMessageSender messageSender = messageSenderFactory.create();
VerifiedMessage verifiedMessage = new VerifiedMessage(canonicalDestination, new IdentityKey(identityKey, 0), verifiedState);
messageSender.sendMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));
}
private void sendFullUpdate() throws IOException, UntrustedIdentityException, InvalidNumberException {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
IdentityDatabase.IdentityReader reader = identityDatabase.readerFor(identityDatabase.getIdentities());
List<VerifiedMessage> verifiedMessages = new LinkedList<>();
try {
IdentityRecord identityRecord;
while (reader != null && (identityRecord = reader.getNext()) != null) {
if (identityRecord.getVerifiedStatus() != VerifiedStatus.DEFAULT) {
Recipient recipient = RecipientFactory.getRecipientForId(context, identityRecord.getRecipientId(), true);
String destination = Util.canonicalizeNumber(context, recipient.getNumber());
VerifiedMessage.VerifiedState verifiedState = getVerifiedState(identityRecord.getVerifiedStatus());
verifiedMessages.add(new VerifiedMessage(destination, identityRecord.getIdentityKey(), verifiedState));
}
}
} finally {
if (reader != null) reader.close();
}
if (!verifiedMessages.isEmpty()) {
SignalServiceMessageSender messageSender = messageSenderFactory.create();
messageSender.sendMessage(SignalServiceSyncMessage.forVerified(verifiedMessages));
}
}
private VerifiedMessage.VerifiedState getVerifiedState(VerifiedStatus status) {
VerifiedMessage.VerifiedState verifiedState;
switch (status) {
case DEFAULT: verifiedState = VerifiedMessage.VerifiedState.DEFAULT; break;
case VERIFIED: verifiedState = VerifiedMessage.VerifiedState.VERIFIED; break;
case UNVERIFIED: verifiedState = VerifiedMessage.VerifiedState.UNVERIFIED; break;
default: throw new AssertionError("Unknown status: " + verifiedStatus);
}
return verifiedState;
}
@Override
public boolean onShouldRetry(Exception exception) {
return exception instanceof PushNetworkException;
}
@Override
public void onAdded() {
}
@Override
public void onCanceled() {
}
}

View File

@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.groups.GroupMessageProcessor;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
@ -44,6 +45,7 @@ import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.libsignal.DuplicateMessageException;
@ -75,13 +77,12 @@ import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.thoughtcrime.securesms.mms.MmsException;
public class PushDecryptJob extends ContextJob {
private static final long serialVersionUID = 2L;
@ -165,9 +166,10 @@ public class PushDecryptJob extends ContextJob {
} else if (content.getSyncMessage().isPresent()) {
SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
if (syncMessage.getSent().isPresent()) handleSynchronizeSentMessage(masterSecret, envelope, syncMessage.getSent().get(), smsMessageId);
else if (syncMessage.getRequest().isPresent()) handleSynchronizeRequestMessage(masterSecret, syncMessage.getRequest().get());
else if (syncMessage.getRead().isPresent()) handleSynchronizeReadMessage(masterSecret, syncMessage.getRead().get(), envelope.getTimestamp());
if (syncMessage.getSent().isPresent()) handleSynchronizeSentMessage(masterSecret, envelope, syncMessage.getSent().get(), smsMessageId);
else if (syncMessage.getRequest().isPresent()) handleSynchronizeRequestMessage(masterSecret, syncMessage.getRequest().get());
else if (syncMessage.getRead().isPresent()) handleSynchronizeReadMessage(masterSecret, syncMessage.getRead().get(), envelope.getTimestamp());
else if (syncMessage.getVerified().isPresent()) handleSynchronizeVerifiedMessage(masterSecret, syncMessage.getVerified().get());
else Log.w(TAG, "Contains no known sync types...");
} else if (content.getCallMessage().isPresent()) {
Log.w(TAG, "Got call message...");
@ -389,6 +391,14 @@ public class PushDecryptJob extends ContextJob {
}
}
private void handleSynchronizeVerifiedMessage(@NonNull MasterSecretUnion masterSecret,
@NonNull List<VerifiedMessage> verifiedMessages)
{
for (VerifiedMessage verifiedMessage : verifiedMessages) {
IdentityUtil.processVerifiedMessage(context, masterSecret, verifiedMessage);
}
}
private void handleSynchronizeSentMessage(@NonNull MasterSecretUnion masterSecret,
@NonNull SignalServiceEnvelope envelope,
@NonNull SentTranscriptMessage message,
@ -430,6 +440,10 @@ public class PushDecryptJob extends ContextJob {
ApplicationContext.getInstance(context)
.getJobManager()
.add(new MultiDeviceContactUpdateJob(getContext()));
ApplicationContext.getInstance(context)
.getJobManager()
.add(new MultiDeviceVerifiedUpdateJob(getContext()));
}
if (message.isGroupsRequest()) {

View File

@ -219,11 +219,6 @@ public class MessageNotifier {
if (isVisible) {
List<MarkedMessageInfo> messageIds = threads.setRead(threadId, false);
MarkReadReceiver.process(context, messageIds);
if (recipients != null && recipients.getPrimaryRecipient() != null) {
DatabaseFactory.getIdentityDatabase(context)
.setSeen(recipients.getPrimaryRecipient().getRecipientId());
}
}
if (!TextSecurePreferences.isNotificationsEnabled(context) ||

View File

@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.jobs.GcmRefreshJob;
import org.thoughtcrime.securesms.push.AccountManagerFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
@ -260,7 +261,7 @@ public class RegistrationService extends Service {
TextSecurePreferences.setWebsocketRegistered(this, true);
DatabaseFactory.getIdentityDatabase(this).saveIdentity(self.getRecipientId(), identityKey.getPublicKey(), true, System.currentTimeMillis(), true, true);
DatabaseFactory.getIdentityDatabase(this).saveIdentity(self.getRecipientId(), identityKey.getPublicKey(), IdentityDatabase.VerifiedStatus.VERIFIED, true, System.currentTimeMillis(), true);
DirectoryHelper.refreshDirectory(this, accountManager, number);
DirectoryRefreshListener.schedule(this);

View File

@ -28,7 +28,6 @@ import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule.SignalMessageSenderFactory;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
@ -69,7 +68,6 @@ import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoRenderer;
import org.webrtc.VideoTrack;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
@ -339,12 +337,6 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
return;
}
if (isUnseenIdentity(this.recipient)) {
insertMissedCall(this.recipient, true);
terminate();
return;
}
timeoutExecutor.schedule(new TimeoutRunnable(this.callId), 2, TimeUnit.MINUTES);
initializeVideo();
@ -370,6 +362,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
@Override
public void onFailureContinue(Throwable error) {
Log.w(TAG, error);
insertMissedCall(recipient, true);
terminate();
}
});
@ -955,28 +948,6 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
else return result;
}
private boolean isUnseenIdentity(@NonNull Recipient recipient) {
Log.w(TAG, "Checking for unseen identity: " + recipient.getRecipientId());
Optional<IdentityRecord> identityRecord = DatabaseFactory.getIdentityDatabase(this).getIdentity(recipient.getRecipientId());
if (!identityRecord.isPresent()) {
throw new AssertionError("Should have an identity record at this point.");
}
if (identityRecord.get().isFirstUse()) {
Log.w(TAG, "Identity is first use...");
return false;
}
Log.w(TAG, "Last seen: " + identityRecord.get().getSeen() + " vs timestamp: " + identityRecord.get().getTimestamp());
if (identityRecord.get().getSeen() >= identityRecord.get().getTimestamp()) {
return false;
}
return true;
}
private long getCallId(Intent intent) {
return intent.getLongExtra(EXTRA_CALL_ID, -1);
}

View File

@ -0,0 +1,15 @@
package org.thoughtcrime.securesms.sms;
public class IncomingIdentityDefaultMessage extends IncomingTextMessage {
public IncomingIdentityDefaultMessage(IncomingTextMessage base) {
super(base, "");
}
@Override
public boolean isIdentityDefault() {
return true;
}
}

View File

@ -0,0 +1,15 @@
package org.thoughtcrime.securesms.sms;
public class IncomingIdentityVerifiedMessage extends IncomingTextMessage {
public IncomingIdentityVerifiedMessage(IncomingTextMessage base) {
super(base, "");
}
@Override
public boolean isIdentityVerified() {
return true;
}
}

View File

@ -227,6 +227,14 @@ public class IncomingTextMessage implements Parcelable {
return false;
}
public boolean isIdentityVerified() {
return false;
}
public boolean isIdentityDefault() {
return false;
}
@Override
public int describeContents() {
return 0;

View File

@ -0,0 +1,20 @@
package org.thoughtcrime.securesms.sms;
import org.thoughtcrime.securesms.recipients.Recipients;
public class OutgoingIdentityDefaultMessage extends OutgoingTextMessage {
public OutgoingIdentityDefaultMessage(Recipients recipients) {
super(recipients, "", -1);
}
@Override
public boolean isIdentityDefault() {
return true;
}
public OutgoingTextMessage withBody(String body) {
return new OutgoingIdentityDefaultMessage(getRecipients());
}
}

View File

@ -0,0 +1,21 @@
package org.thoughtcrime.securesms.sms;
import org.thoughtcrime.securesms.recipients.Recipients;
public class OutgoingIdentityVerifiedMessage extends OutgoingTextMessage {
public OutgoingIdentityVerifiedMessage(Recipients recipients) {
super(recipients, "", -1);
}
@Override
public boolean isIdentityVerified() {
return true;
}
@Override
public OutgoingTextMessage withBody(String body) {
return new OutgoingIdentityVerifiedMessage(getRecipients());
}
}

View File

@ -60,6 +60,14 @@ public class OutgoingTextMessage {
return false;
}
public boolean isIdentityVerified() {
return false;
}
public boolean isIdentityDefault() {
return false;
}
public static OutgoingTextMessage from(SmsMessageRecord record) {
if (record.isSecure()) {
return new OutgoingEncryptedMessage(record.getRecipients(), record.getBody().getBody(), record.getExpiresIn());

View File

@ -2,61 +2,59 @@ package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.annotation.UiThread;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecretUnion;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.MessagingDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.IncomingIdentityDefaultMessage;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.sms.IncomingIdentityVerifiedMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.OutgoingIdentityDefaultMessage;
import org.thoughtcrime.securesms.sms.OutgoingIdentityVerifiedMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.util.List;
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
public class IdentityUtil {
private static final String TAG = IdentityUtil.class.getSimpleName();
@UiThread
public static ListenableFuture<Optional<IdentityKey>> getRemoteIdentityKey(final Context context,
final MasterSecret masterSecret,
final Recipient recipient)
{
final SettableFuture<Optional<IdentityKey>> future = new SettableFuture<>();
public static ListenableFuture<Optional<IdentityRecord>> getRemoteIdentityKey(final Context context, final Recipient recipient) {
final SettableFuture<Optional<IdentityRecord>> future = new SettableFuture<>();
new AsyncTask<Recipient, Void, Optional<IdentityKey>>() {
new AsyncTask<Recipient, Void, Optional<IdentityRecord>>() {
@Override
protected Optional<IdentityKey> doInBackground(Recipient... recipient) {
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(recipient[0].getNumber(), SignalServiceAddress.DEFAULT_DEVICE_ID);
SessionRecord record = sessionStore.loadSession(axolotlAddress);
if (record == null) {
return Optional.absent();
}
return Optional.fromNullable(record.getSessionState().getRemoteIdentityKey());
protected Optional<IdentityRecord> doInBackground(Recipient... recipient) {
return DatabaseFactory.getIdentityDatabase(context)
.getIdentity(recipient[0].getRecipientId());
}
@Override
protected void onPostExecute(Optional<IdentityKey> result) {
protected void onPostExecute(Optional<IdentityRecord> result) {
future.set(result);
}
}.execute(recipient);
@ -64,6 +62,102 @@ public class IdentityUtil {
return future;
}
public static void markIdentityVerified(Context context, MasterSecretUnion masterSecret,
Recipient recipient, boolean verified, boolean remote)
{
long time = System.currentTimeMillis();
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
Recipients recipients = RecipientFactory.getRecipientsFor(context, recipient, true);
GroupDatabase.Reader reader = groupDatabase.getGroups();
String number = recipient.getNumber();
try {
number = Util.canonicalizeNumber(context, number);
} catch (InvalidNumberException e) {
Log.w(TAG, e);
}
GroupDatabase.GroupRecord groupRecord;
while ((groupRecord = reader.getNext()) != null) {
if (groupRecord.getMembers().contains(number) && groupRecord.isActive()) {
SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId());
if (remote) {
IncomingTextMessage incoming = new IncomingTextMessage(number, 1, time, null, Optional.of(group), 0);
if (verified) incoming = new IncomingIdentityVerifiedMessage(incoming);
else incoming = new IncomingIdentityDefaultMessage(incoming);
smsDatabase.insertMessageInbox(incoming);
} else {
Recipients groupRecipients = RecipientFactory.getRecipientsFromString(context, GroupUtil.getEncodedId(group.getGroupId()), true);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipients);
OutgoingTextMessage outgoing ;
if (verified) outgoing = new OutgoingIdentityVerifiedMessage(recipients);
else outgoing = new OutgoingIdentityDefaultMessage(recipients);
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageOutbox(masterSecret, threadId, outgoing, false, time, null);
}
}
}
if (remote) {
IncomingTextMessage incoming = new IncomingTextMessage(number, 1, time, null, Optional.<SignalServiceGroup>absent(), 0);
if (verified) incoming = new IncomingIdentityVerifiedMessage(incoming);
else incoming = new IncomingIdentityDefaultMessage(incoming);
smsDatabase.insertMessageInbox(incoming);
} else {
OutgoingTextMessage outgoing;
if (verified) outgoing = new OutgoingIdentityVerifiedMessage(recipients);
else outgoing = new OutgoingIdentityDefaultMessage(recipients);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
Log.w(TAG, "Inserting verified outbox...");
DatabaseFactory.getEncryptingSmsDatabase(context)
.insertMessageOutbox(masterSecret, threadId, outgoing, false, time, null);
}
}
public static void processVerifiedMessage(Context context, MasterSecretUnion masterSecret, VerifiedMessage verifiedMessage) {
synchronized (SESSION_LOCK) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
Recipient recipient = RecipientFactory.getRecipientsFromString(context, verifiedMessage.getDestination(), true).getPrimaryRecipient();
Optional<IdentityRecord> identityRecord = identityDatabase.getIdentity(recipient.getRecipientId());
if (!identityRecord.isPresent() && verifiedMessage.getVerified() == VerifiedMessage.VerifiedState.DEFAULT) {
Log.w(TAG, "No existing record for default status");
return;
}
if (identityRecord.isPresent() &&
identityRecord.get().getIdentityKey().equals(verifiedMessage.getIdentityKey()) &&
identityRecord.get().getVerifiedStatus() != IdentityDatabase.VerifiedStatus.DEFAULT &&
verifiedMessage.getVerified() == VerifiedMessage.VerifiedState.DEFAULT)
{
identityDatabase.setVerified(recipient.getRecipientId(), identityRecord.get().getIdentityKey(), IdentityDatabase.VerifiedStatus.DEFAULT);
markIdentityVerified(context, masterSecret, recipient, false, true);
}
if (verifiedMessage.getVerified() == VerifiedMessage.VerifiedState.VERIFIED &&
(!identityRecord.isPresent() ||
(identityRecord.isPresent() && !identityRecord.get().getIdentityKey().equals(verifiedMessage.getIdentityKey())) ||
(identityRecord.isPresent() && identityRecord.get().getVerifiedStatus() != IdentityDatabase.VerifiedStatus.VERIFIED)))
{
identityDatabase.saveIdentity(recipient.getRecipientId(), verifiedMessage.getIdentityKey(),
IdentityDatabase.VerifiedStatus.VERIFIED, false, System.currentTimeMillis(), true);
markIdentityVerified(context, masterSecret, recipient, true, true);
}
}
}
public static void markIdentityUpdate(Context context, Recipient recipient) {
long time = System.currentTimeMillis();
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
@ -98,4 +192,62 @@ public class IdentityUtil {
MessageNotifier.updateNotification(context, null, insertResult.get().getThreadId());
}
}
public static @Nullable String getUnverifiedBannerDescription(@NonNull Context context,
@NonNull List<Recipient> unverified)
{
return getPluralizedIdentityDescription(context, unverified,
R.string.IdentityUtil_unverified_banner_one,
R.string.IdentityUtil_unverified_banner_two,
R.string.IdentityUtil_unverified_banner_many);
}
public static @Nullable String getUnverifiedSendDialogDescription(@NonNull Context context,
@NonNull List<Recipient> unverified)
{
return getPluralizedIdentityDescription(context, unverified,
R.string.IdentityUtil_unverified_dialog_one,
R.string.IdentityUtil_unverified_dialog_two,
R.string.IdentityUtil_unverified_dialog_many);
}
public static @Nullable String getUntrustedSendDialogDescription(@NonNull Context context,
@NonNull List<Recipient> untrusted)
{
return getPluralizedIdentityDescription(context, untrusted,
R.string.IdentityUtil_untrusted_dialog_one,
R.string.IdentityUtil_untrusted_dialog_two,
R.string.IdentityUtil_untrusted_dialog_many);
}
private static @Nullable String getPluralizedIdentityDescription(@NonNull Context context,
@NonNull List<Recipient> recipients,
@StringRes int resourceOne,
@StringRes int resourceTwo,
@StringRes int resourceMany)
{
if (recipients.isEmpty()) return null;
if (recipients.size() == 1) {
String name = recipients.get(0).toShortString();
return context.getString(resourceOne, name);
} else {
String firstName = recipients.get(0).toShortString();
String secondName = recipients.get(1).toShortString();
if (recipients.size() == 2) {
return context.getString(resourceTwo, firstName, secondName);
} else {
String nMore;
if (recipients.size() == 3) {
nMore = context.getResources().getQuantityString(R.plurals.identity_others, 1);
} else {
nMore = context.getResources().getQuantityString(R.plurals.identity_others, recipients.size() - 2);
}
return context.getString(resourceMany, firstName, secondName, nMore);
}
}
}
}

View File

@ -32,8 +32,9 @@ public class VerifySpan extends ClickableSpan {
@Override
public void onClick(View widget) {
Intent intent = new Intent(context, VerifyIdentityActivity.class);
intent.putExtra(VerifyIdentityActivity.RECIPIENT_ID, recipientId);
intent.putExtra(VerifyIdentityActivity.RECIPIENT_IDENTITY, new IdentityKeyParcelable(identityKey));
intent.putExtra(VerifyIdentityActivity.RECIPIENT_ID_EXTRA, recipientId);
intent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(identityKey));
intent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, false);
context.startActivity(intent);
}
}