diff --git a/build.gradle b/build.gradle index 92b3f51acb..dbf4e912ec 100644 --- a/build.gradle +++ b/build.gradle @@ -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', diff --git a/res/drawable-hdpi/ic_check_circle_white_18dp.png b/res/drawable-hdpi/ic_check_circle_white_18dp.png new file mode 100644 index 0000000000..81c6ee054f Binary files /dev/null and b/res/drawable-hdpi/ic_check_circle_white_18dp.png differ diff --git a/res/drawable-mdpi/ic_check_circle_white_18dp.png b/res/drawable-mdpi/ic_check_circle_white_18dp.png new file mode 100644 index 0000000000..0533ac82ea Binary files /dev/null and b/res/drawable-mdpi/ic_check_circle_white_18dp.png differ diff --git a/res/drawable-xhdpi/ic_check_circle_white_18dp.png b/res/drawable-xhdpi/ic_check_circle_white_18dp.png new file mode 100644 index 0000000000..e190a5c537 Binary files /dev/null and b/res/drawable-xhdpi/ic_check_circle_white_18dp.png differ diff --git a/res/drawable-xxhdpi/ic_check_circle_white_18dp.png b/res/drawable-xxhdpi/ic_check_circle_white_18dp.png new file mode 100644 index 0000000000..8b16b1c2e1 Binary files /dev/null and b/res/drawable-xxhdpi/ic_check_circle_white_18dp.png differ diff --git a/res/drawable-xxxhdpi/ic_check_circle_white_18dp.png b/res/drawable-xxxhdpi/ic_check_circle_white_18dp.png new file mode 100644 index 0000000000..4f967453e4 Binary files /dev/null and b/res/drawable-xxxhdpi/ic_check_circle_white_18dp.png differ diff --git a/res/layout/conversation_activity.xml b/res/layout/conversation_activity.xml index 26492329c5..5e5f06aef8 100644 --- a/res/layout/conversation_activity.xml +++ b/res/layout/conversation_activity.xml @@ -23,6 +23,12 @@ android:clipToPadding="false" android:clipChildren="false"> + + + diff --git a/res/layout/conversation_title_view.xml b/res/layout/conversation_title_view.xml index 116833d20c..1c5478874b 100644 --- a/res/layout/conversation_title_view.xml +++ b/res/layout/conversation_title_view.xml @@ -21,14 +21,31 @@ style="@style/TextSecure.TitleTextStyle" tools:ignore="UnusedAttribute"/> - + + + + + + + \ No newline at end of file diff --git a/res/layout/unverified_banner_view.xml b/res/layout/unverified_banner_view.xml new file mode 100644 index 0000000000..d6a6ab318d --- /dev/null +++ b/res/layout/unverified_banner_view.xml @@ -0,0 +1,42 @@ + + + + + + + + + + diff --git a/res/layout/verify_display_fragment.xml b/res/layout/verify_display_fragment.xml index 2c7603c239..2c94a73efc 100644 --- a/res/layout/verify_display_fragment.xml +++ b/res/layout/verify_display_fragment.xml @@ -12,17 +12,25 @@ android:background="?verification_background" android:orientation="vertical"> - + + + + android:visibility="invisible" + tools:src="@drawable/splash_logo" + tools:visibility="invisible"/> + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 3fab75e9f8..2468a5ff9c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -421,6 +421,10 @@ You set disappearing message time to %1$s. %1$s set disappearing message time to %2$s. Your safety number with %s has changed. + You marked your safety number with %s verified + You marked your safety number with %s verified from another device + You marked your safety number with %s unverified + You marked your safety number with %s unverified from another device @@ -594,6 +598,8 @@ Disappearing message time set to %s Safety number changed Your safety number with %s has changed. + You marked verified + You marked unverified Signal update @@ -851,6 +857,24 @@ %dw + + + Your safety number with %s has changed and is no longer verified + Your safety numbers with %1$s and %2$s are no longer verified + Your safety numbers with %1$s, %2$s, and %3$s are no longer verified + + 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. + 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. + 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. + + Your safety number with %s just changed. + Your safety number with %1$s and %2$s just changed. + Your safety number with %1$s, %2$s, and %3$s just changed. + + + %d other + %d others + Search GIFs and stickers @@ -1022,8 +1046,10 @@ Add members - Learn more about verifying safety numbers]]> + Learn more.]]> Tap to scan + Loading... + Verified Share safety number @@ -1375,6 +1401,10 @@ Transport icon + Send message? + Send + Send message? + Send diff --git a/src/org/thoughtcrime/securesms/ConfirmIdentityDialog.java b/src/org/thoughtcrime/securesms/ConfirmIdentityDialog.java index 334ce1fca7..a926447723 100644 --- a/src/org/thoughtcrime/securesms/ConfirmIdentityDialog.java +++ b/src/org/thoughtcrime/securesms/ConfirmIdentityDialog.java @@ -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); diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index e92015b7fe..6216b4d122 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -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; + 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; + private Stub 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 unverifiedRecipients = identityRecords.getUnverifiedRecipients(this); + List 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() { + @Override + public void onSuccess(Boolean result) { + sendMessage(); + } + + @Override + public void onFailure(ExecutionException e) { + throw new AssertionError(e); + } + }); + } + }).show(); + } + + private void handleUntrustedRecipients() { + List untrustedRecipients = identityRecords.getUntrustedRecipients(this); + List 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() { + @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 initializeIdentityRecords() { + final SettableFuture future = new SettableFuture<>(); + + new AsyncTask>() { + @Override + protected @NonNull Pair 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 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() { - @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 unverifiedIdentities) { + final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(ConversationActivity.this); + + new AsyncTask() { + @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 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 @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; diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java index e2ea374297..5f65c94339 100644 --- a/src/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationFragment.java @@ -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; diff --git a/src/org/thoughtcrime/securesms/ConversationTitleView.java b/src/org/thoughtcrime/securesms/ConversationTitleView.java index 64e593b98a..93676a00bd 100644 --- a/src/org/thoughtcrime/securesms/ConversationTitleView.java +++ b/src/org/thoughtcrime/securesms/ConversationTitleView.java @@ -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); diff --git a/src/org/thoughtcrime/securesms/ConversationUpdateItem.java b/src/org/thoughtcrime/securesms/ConversationUpdateItem.java index a7232f3d08..cbf321f3cc 100644 --- a/src/org/thoughtcrime/securesms/ConversationUpdateItem.java +++ b/src/org/thoughtcrime/securesms/ConversationUpdateItem.java @@ -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>() { + IdentityUtil.getRemoteIdentityKey(getContext(), sender).addListener(new ListenableFuture.Listener>() { @Override - public void onSuccess(Optional result) { + public void onSuccess(Optional 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); } diff --git a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java index 4e4d3e0afa..7b844b0f0b 100644 --- a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java +++ b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java @@ -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>() { + IdentityUtil.getRemoteIdentityKey(getActivity(), recipients.getPrimaryRecipient()).addListener(new ListenableFuture.Listener>() { @Override - public void onSuccess(Optional result) { + public void onSuccess(Optional 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; diff --git a/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java b/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java index b17ed6280a..8d6e060240 100644 --- a/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java +++ b/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java @@ -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() { + @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= 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() { + 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() { + @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 { diff --git a/src/org/thoughtcrime/securesms/WebRtcCallActivity.java b/src/org/thoughtcrime/securesms/WebRtcCallActivity.java index 79058b9e2d..55d0bb7189 100644 --- a/src/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/src/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -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); diff --git a/src/org/thoughtcrime/securesms/components/identity/UntrustedSendDialog.java b/src/org/thoughtcrime/securesms/components/identity/UntrustedSendDialog.java new file mode 100644 index 0000000000..0bd6268a4b --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/identity/UntrustedSendDialog.java @@ -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 untrustedRecords; + private final ResendListener resendListener; + + public UntrustedSendDialog(@NonNull Context context, + @NonNull String message, + @NonNull List 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() { + @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(); + } +} diff --git a/src/org/thoughtcrime/securesms/components/identity/UnverifiedBannerView.java b/src/org/thoughtcrime/securesms/components/identity/UnverifiedBannerView.java new file mode 100644 index 0000000000..753d8df71c --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/identity/UnverifiedBannerView.java @@ -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 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 unverifiedIdentities); + } + + public interface ClickListener { + public void onClicked(List unverifiedIdentities); + } + +} diff --git a/src/org/thoughtcrime/securesms/components/identity/UnverifiedSendDialog.java b/src/org/thoughtcrime/securesms/components/identity/UnverifiedSendDialog.java new file mode 100644 index 0000000000..bf1e0cf300 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/identity/UnverifiedSendDialog.java @@ -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 untrustedRecords; + private final ResendListener resendListener; + + public UnverifiedSendDialog(@NonNull Context context, + @NonNull String message, + @NonNull List 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() { + @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(); + } +} diff --git a/src/org/thoughtcrime/securesms/crypto/SessionUtil.java b/src/org/thoughtcrime/securesms/crypto/SessionUtil.java index d1974face3..32d9c203f0 100644 --- a/src/org/thoughtcrime/securesms/crypto/SessionUtil.java +++ b/src/org/thoughtcrime/securesms/crypto/SessionUtil.java @@ -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 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); + } + } + } + + } } diff --git a/src/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java b/src/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java index 1e0524585e..5ecb7d5bd1 100644 --- a/src/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java +++ b/src/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java @@ -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) && diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index b7dc02514b..d752698607 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -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(); diff --git a/src/org/thoughtcrime/securesms/database/IdentityDatabase.java b/src/org/thoughtcrime/securesms/database/IdentityDatabase.java index 60d9223569..e477e0ac0f 100644 --- a/src/org/thoughtcrime/securesms/database/IdentityDatabase.java +++ b/src/org/thoughtcrime/securesms/database/IdentityDatabase.java @@ -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 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(); + } } } diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java index 79a904a680..59849c96ff 100644 --- a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -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) { diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java index ec400b249e..d830bd2b32 100644 --- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -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); diff --git a/src/org/thoughtcrime/securesms/database/identity/IdentityRecordList.java b/src/org/thoughtcrime/securesms/database/identity/IdentityRecordList.java new file mode 100644 index 0000000000..ad7679c350 --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/identity/IdentityRecordList.java @@ -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 identityRecords = new LinkedList<>(); + + public void add(Optional 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 getUntrustedRecords() { + List results = new LinkedList<>(); + + for (IdentityRecord identityRecord : identityRecords) { + if (isUntrusted(identityRecord)) { + results.add(identityRecord); + } + } + + return results; + } + + public List getUntrustedRecipients(Context context) { + List untrusted = new LinkedList<>(); + + for (IdentityRecord identityRecord : identityRecords) { + if (isUntrusted(identityRecord)) { + untrusted.add(RecipientFactory.getRecipientForId(context, identityRecord.getRecipientId(), false)); + } + } + + return untrusted; + } + + public List getUnverifiedRecords() { + List results = new LinkedList<>(); + + for (IdentityRecord identityRecord : identityRecords) { + if (identityRecord.getVerifiedStatus() == VerifiedStatus.UNVERIFIED) { + results.add(identityRecord); + } + } + + return results; + } + + public List getUnverifiedRecipients(Context context) { + List 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); + } + +} diff --git a/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java index df6f6f80c5..f480033a10 100644 --- a/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java @@ -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() { diff --git a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java index 7e77e9c5d8..ff269e04d0 100644 --- a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -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() { diff --git a/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java index 796d08d48b..2a8ab4a3b8 100644 --- a/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java @@ -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()) { diff --git a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java index 18cf7e0aa0..8f21299871 100644 --- a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -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))); diff --git a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java index f432b0ef90..97f4ec7358 100644 --- a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java +++ b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java @@ -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; diff --git a/src/org/thoughtcrime/securesms/jobs/MultiDeviceVerifiedUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/MultiDeviceVerifiedUpdateJob.java new file mode 100644 index 0000000000..92028384b6 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/MultiDeviceVerifiedUpdateJob.java @@ -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 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() { + + } +} diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 9b2f18aa42..5c09002c10 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -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 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()) { diff --git a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java index ad98e2d2ea..6f8f214281 100644 --- a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java +++ b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java @@ -219,11 +219,6 @@ public class MessageNotifier { if (isVisible) { List 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) || diff --git a/src/org/thoughtcrime/securesms/service/RegistrationService.java b/src/org/thoughtcrime/securesms/service/RegistrationService.java index 5309daf444..d1e1ac4c2b 100644 --- a/src/org/thoughtcrime/securesms/service/RegistrationService.java +++ b/src/org/thoughtcrime/securesms/service/RegistrationService.java @@ -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); diff --git a/src/org/thoughtcrime/securesms/service/WebRtcCallService.java b/src/org/thoughtcrime/securesms/service/WebRtcCallService.java index 7e952ea5db..27db22f284 100644 --- a/src/org/thoughtcrime/securesms/service/WebRtcCallService.java +++ b/src/org/thoughtcrime/securesms/service/WebRtcCallService.java @@ -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 = 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); } diff --git a/src/org/thoughtcrime/securesms/sms/IncomingIdentityDefaultMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingIdentityDefaultMessage.java new file mode 100644 index 0000000000..fc9c75e752 --- /dev/null +++ b/src/org/thoughtcrime/securesms/sms/IncomingIdentityDefaultMessage.java @@ -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; + } + +} diff --git a/src/org/thoughtcrime/securesms/sms/IncomingIdentityVerifiedMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingIdentityVerifiedMessage.java new file mode 100644 index 0000000000..23a5d51900 --- /dev/null +++ b/src/org/thoughtcrime/securesms/sms/IncomingIdentityVerifiedMessage.java @@ -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; + } + +} diff --git a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java index 2029122e5d..befd029f15 100644 --- a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java +++ b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java @@ -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; diff --git a/src/org/thoughtcrime/securesms/sms/OutgoingIdentityDefaultMessage.java b/src/org/thoughtcrime/securesms/sms/OutgoingIdentityDefaultMessage.java new file mode 100644 index 0000000000..df728de264 --- /dev/null +++ b/src/org/thoughtcrime/securesms/sms/OutgoingIdentityDefaultMessage.java @@ -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()); + } +} diff --git a/src/org/thoughtcrime/securesms/sms/OutgoingIdentityVerifiedMessage.java b/src/org/thoughtcrime/securesms/sms/OutgoingIdentityVerifiedMessage.java new file mode 100644 index 0000000000..28683eb4f1 --- /dev/null +++ b/src/org/thoughtcrime/securesms/sms/OutgoingIdentityVerifiedMessage.java @@ -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()); + } +} diff --git a/src/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java b/src/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java index 2a90dd0f42..760e1d9710 100644 --- a/src/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java +++ b/src/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java @@ -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()); diff --git a/src/org/thoughtcrime/securesms/util/IdentityUtil.java b/src/org/thoughtcrime/securesms/util/IdentityUtil.java index 6dd750281d..69f8863f9c 100644 --- a/src/org/thoughtcrime/securesms/util/IdentityUtil.java +++ b/src/org/thoughtcrime/securesms/util/IdentityUtil.java @@ -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> getRemoteIdentityKey(final Context context, - final MasterSecret masterSecret, - final Recipient recipient) - { - final SettableFuture> future = new SettableFuture<>(); + public static ListenableFuture> getRemoteIdentityKey(final Context context, final Recipient recipient) { + final SettableFuture> future = new SettableFuture<>(); - new AsyncTask>() { + new AsyncTask>() { @Override - protected Optional 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 doInBackground(Recipient... recipient) { + return DatabaseFactory.getIdentityDatabase(context) + .getIdentity(recipient[0].getRecipientId()); } @Override - protected void onPostExecute(Optional result) { + protected void onPostExecute(Optional 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.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 = 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 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 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 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 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); + } + } + } } diff --git a/src/org/thoughtcrime/securesms/util/VerifySpan.java b/src/org/thoughtcrime/securesms/util/VerifySpan.java index 4e750ac9a1..d450f6688d 100644 --- a/src/org/thoughtcrime/securesms/util/VerifySpan.java +++ b/src/org/thoughtcrime/securesms/util/VerifySpan.java @@ -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); } }