diff --git a/build.gradle b/build.gradle index a3ae8f966e..e96c363d3c 100644 --- a/build.gradle +++ b/build.gradle @@ -2,10 +2,11 @@ buildscript { repositories { maven { url "https://repo1.maven.org/maven2" + jcenter() } } dependencies { - classpath 'com.android.tools.build:gradle:1.2.3' + classpath 'com.android.tools.build:gradle:2.1.3' classpath files('libs/gradle-witness.jar') } } @@ -159,7 +160,7 @@ dependencyVerification { android { compileSdkVersion 22 - buildToolsVersion '22.0.1' + buildToolsVersion '23.0.2' dexOptions { javaMaxHeapSize "4g" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 513819a3ab..7dc78b5e79 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Jul 22 14:31:11 PDT 2015 +#Sun Aug 28 20:14:40 PDT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/libs/gradle-witness.jar b/libs/gradle-witness.jar index 561041d366..6ab83e5962 100644 Binary files a/libs/gradle-witness.jar and b/libs/gradle-witness.jar differ diff --git a/res/drawable-hdpi/ic_security_white_24dp.png b/res/drawable-hdpi/ic_security_white_24dp.png new file mode 100644 index 0000000000..262800a4d8 Binary files /dev/null and b/res/drawable-hdpi/ic_security_white_24dp.png differ diff --git a/res/drawable-mdpi/ic_security_white_24dp.png b/res/drawable-mdpi/ic_security_white_24dp.png new file mode 100644 index 0000000000..44ee7346e3 Binary files /dev/null and b/res/drawable-mdpi/ic_security_white_24dp.png differ diff --git a/res/drawable-xhdpi/ic_security_white_24dp.png b/res/drawable-xhdpi/ic_security_white_24dp.png new file mode 100644 index 0000000000..7e306c303c Binary files /dev/null and b/res/drawable-xhdpi/ic_security_white_24dp.png differ diff --git a/res/drawable-xxhdpi/ic_security_white_24dp.png b/res/drawable-xxhdpi/ic_security_white_24dp.png new file mode 100644 index 0000000000..7bcb2fd013 Binary files /dev/null and b/res/drawable-xxhdpi/ic_security_white_24dp.png differ diff --git a/res/drawable-xxxhdpi/ic_security_white_24dp.png b/res/drawable-xxxhdpi/ic_security_white_24dp.png new file mode 100644 index 0000000000..b1eddbd6c3 Binary files /dev/null and b/res/drawable-xxxhdpi/ic_security_white_24dp.png differ diff --git a/res/layout/verify_display_fragment.xml b/res/layout/verify_display_fragment.xml index 633756b39d..14fffd7267 100644 --- a/res/layout/verify_display_fragment.xml +++ b/res/layout/verify_display_fragment.xml @@ -139,7 +139,8 @@ - The - identifying key material for %1$s has changed. This could either mean that someone is trying to - intercept your communication, or that %2$s simply re-installed Signal and now has a new - identity key. + safety numbers for %1$s have changed. This could either mean that someone is trying to + intercept your communication, or that %2$s simply re-installed Signal. You may wish to verify - this contact. + safety numbers for this contact. Accept @@ -355,7 +354,7 @@ Failed to send - New identity + New safety numbers Error storing MMS! @@ -536,8 +535,7 @@ Received key exchange message for invalid protocol version. - Received message with unknown identity key. Tap to process and display. - Received updated but unknown identity information. Tap to validate identity. + Received message with new safety numbers. Tap to process and display. You reset the secure session. %s reset the secure session. Duplicate message. @@ -558,17 +556,6 @@ You\'re attempting to verify security numbers with %1$s, but scanned %2$s instead. The scanned QR code is not a correctly formatted security number verification code. Please try scanning again. - - - You do not have an identity key. - Scan contact\'s QR code - Display your QR code - WARNING, the scanned key DOES NOT match! - NOT verified! - The scanned key matches! - Verified! - Your identity fingerprint - Initiate despite existing request? Send @@ -833,7 +820,7 @@ Block Color Color for this contact - Verify identity + Verify safety numbers Signal Call @@ -930,7 +917,7 @@ Add member - Scan the code on your contact\'s phone, or ask them to scan your code, to verify that your messages are end-to-end encrypted. You can alternately compare the number above. + If you wish to verify the security of your end-to-end encryption with %s, compare the numbers above with the numbers on their device. Alternately, you can scan the code on their phone, or ask them to scan your code. Tap to scan @@ -947,9 +934,8 @@ Enter passphrase Select contacts Signal detected - Public identity key Change passphrase - Verify identity + Verify safety numbers Submit debug log Media preview All images @@ -961,7 +947,6 @@ Import / export - Your identity key Use default Use custom @@ -1084,6 +1069,8 @@ \'WiFi Calling\' compatibility mode Enable if your device uses SMS/MMS delivery over WiFi (only enable when \'WiFi Calling\' is enabled on your device) Blocked contacts + Safety numbers approval + Require approval of new safety numbers when they change Display in notifications When using mobile data When using Wi-Fi @@ -1156,8 +1143,6 @@ New conversation - Security - Verify identity Reset secure session diff --git a/res/xml/preferences_app_protection.xml b/res/xml/preferences_app_protection.xml index 908e70ab7d..0cdee84f75 100644 --- a/res/xml/preferences_app_protection.xml +++ b/res/xml/preferences_app_protection.xml @@ -28,6 +28,12 @@ android:title="@string/preferences__screen_security" android:summary="@string/preferences__disable_screen_security_to_allow_screen_shots" /> + + diff --git a/res/xml/recipient_preferences.xml b/res/xml/recipient_preferences.xml index 5185e1fcc1..0744845c2f 100644 --- a/res/xml/recipient_preferences.xml +++ b/res/xml/recipient_preferences.xml @@ -37,7 +37,7 @@ app:numColumns="5" /> diff --git a/src/org/thoughtcrime/securesms/ConversationAdapter.java b/src/org/thoughtcrime/securesms/ConversationAdapter.java index 91739f9054..88f550118a 100644 --- a/src/org/thoughtcrime/securesms/ConversationAdapter.java +++ b/src/org/thoughtcrime/securesms/ConversationAdapter.java @@ -195,9 +195,9 @@ public class ConversationAdapter String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT)); MessageRecord messageRecord = getMessageRecord(id, cursor, type); - if (messageRecord.isGroupAction() || messageRecord.isCallLog() || messageRecord.isJoined() || - messageRecord.isExpirationTimerUpdate() || messageRecord.isEndSession()) - { + if (messageRecord.isGroupAction() || messageRecord.isCallLog() || messageRecord.isJoined() || + messageRecord.isExpirationTimerUpdate() || messageRecord.isEndSessin() || messageRecord.isIdentityUpdate()) + { return MESSAGE_TYPE_UPDATE; } else if (messageRecord.isOutgoing()) { return MESSAGE_TYPE_OUTGOING; diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java index 292d84af2a..6991615476 100644 --- a/src/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationFragment.java @@ -181,7 +181,7 @@ public class ConversationFragment extends Fragment for (MessageRecord messageRecord : messageRecords) { if (messageRecord.isGroupAction() || messageRecord.isCallLog() || messageRecord.isJoined() || messageRecord.isExpirationTimerUpdate() || - messageRecord.isEndSession()) + messageRecord.isEndSession() || messageRecord.isIdentityUpdate()) { actionMessage = true; break; diff --git a/src/org/thoughtcrime/securesms/ConversationUpdateItem.java b/src/org/thoughtcrime/securesms/ConversationUpdateItem.java index 1e27a50d28..a7232f3d08 100644 --- a/src/org/thoughtcrime/securesms/ConversationUpdateItem.java +++ b/src/org/thoughtcrime/securesms/ConversationUpdateItem.java @@ -6,28 +6,39 @@ import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.AttributeSet; +import android.util.Log; import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.util.DateUtils; 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; import java.util.Set; +import java.util.concurrent.ExecutionException; public class ConversationUpdateItem extends LinearLayout - implements Recipients.RecipientsModifiedListener, Recipient.RecipientModifiedListener, BindableConversationItem, View.OnClickListener + implements Recipients.RecipientsModifiedListener, Recipient.RecipientModifiedListener, BindableConversationItem { private static final String TAG = ConversationUpdateItem.class.getSimpleName(); + private MasterSecret masterSecret; + private Set batchSelected; + private ImageView icon; private TextView body; private TextView date; @@ -51,7 +62,7 @@ public class ConversationUpdateItem extends LinearLayout this.body = (TextView)findViewById(R.id.conversation_update_body); this.date = (TextView)findViewById(R.id.conversation_update_date); - setOnClickListener(this); + this.setOnClickListener(new InternalClickListener(null)); } @Override @@ -61,13 +72,10 @@ public class ConversationUpdateItem extends LinearLayout @NonNull Set batchSelected, @NonNull Recipients conversationRecipients) { - bind(messageRecord, locale); + this.masterSecret = masterSecret; + this.batchSelected = batchSelected; - if (batchSelected.contains(messageRecord)) { - setSelected(true); - } else { - setSelected(false); - } + bind(messageRecord, locale); } @Override @@ -87,7 +95,11 @@ public class ConversationUpdateItem extends LinearLayout else if (messageRecord.isJoined()) setJoinedRecord(messageRecord); else if (messageRecord.isExpirationTimerUpdate()) setTimerRecord(messageRecord); else if (messageRecord.isEndSession()) setEndSessionRecord(messageRecord); + else if (messageRecord.isIdentityUpdate()) setIdentityRecord(messageRecord); else throw new AssertionError("Neither group nor log nor joined."); + + if (batchSelected.contains(messageRecord)) setSelected(true); + else setSelected(false); } private void setCallRecord(MessageRecord messageRecord) { @@ -113,6 +125,13 @@ public class ConversationUpdateItem extends LinearLayout date.setVisibility(View.GONE); } + private void setIdentityRecord(final MessageRecord messageRecord) { + icon.setImageResource(R.drawable.ic_security_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(); @@ -153,14 +172,8 @@ public class ConversationUpdateItem extends LinearLayout } @Override - public void onClick(View v) { - if (messageRecord.isIdentityUpdate()) { - Intent intent = new Intent(getContext(), RecipientPreferenceActivity.class); - intent.putExtra(RecipientPreferenceActivity.RECIPIENTS_EXTRA, - new long[] {messageRecord.getIndividualRecipient().getRecipientId()}); - - getContext().startActivity(intent); - } + public void setOnClickListener(View.OnClickListener l) { + super.setOnClickListener(new InternalClickListener(l)); } @Override @@ -169,4 +182,42 @@ public class ConversationUpdateItem extends LinearLayout sender.removeListener(this); } } + + private class InternalClickListener implements View.OnClickListener { + + @Nullable private final View.OnClickListener parent; + + public InternalClickListener(@Nullable View.OnClickListener parent) { + this.parent = parent; + } + + @Override + public void onClick(View v) { + if (!messageRecord.isIdentityUpdate() || !batchSelected.isEmpty()) { + if (parent != null) parent.onClick(v); + return; + } + + final Recipient sender = ConversationUpdateItem.this.sender; + + IdentityUtil.getRemoteIdentityKey(getContext(), masterSecret, sender).addListener(new ListenableFuture.Listener>() { + @Override + 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())); + + getContext().startActivity(intent); + } + } + + @Override + public void onFailure(ExecutionException e) { + Log.w(TAG, e); + } + }); + } + } + } diff --git a/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java b/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java index 9f4f30baf5..9c62e4c17b 100644 --- a/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java +++ b/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java @@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; import org.thoughtcrime.securesms.jobs.PushDecryptJob; import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; import org.thoughtcrime.securesms.notifications.MessageNotifier; +import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.VersionTracker; @@ -68,6 +69,7 @@ public class DatabaseUpgradeActivity extends BaseActivity { public static final int CONTACTS_ACCOUNT_VERSION = 136; public static final int MEDIA_DOWNLOAD_CONTROLS_VERSION = 151; public static final int REDPHONE_SUPPORT_VERSION = 157; + public static final int FINGERPRINTS_NON_BLOCKING_VESRION = 197; private static final SortedSet UPGRADE_VERSIONS = new TreeSet() {{ add(NO_MORE_KEY_EXCHANGE_PREFIX_VERSION); @@ -81,6 +83,7 @@ public class DatabaseUpgradeActivity extends BaseActivity { add(MIGRATE_SESSION_PLAINTEXT); add(MEDIA_DOWNLOAD_CONTROLS_VERSION); add(REDPHONE_SUPPORT_VERSION); + add(FINGERPRINTS_NON_BLOCKING_VESRION); }}; private MasterSecret masterSecret; @@ -231,6 +234,10 @@ public class DatabaseUpgradeActivity extends BaseActivity { .add(new DirectoryRefreshJob(getApplicationContext())); } + if (params[0] < FINGERPRINTS_NON_BLOCKING_VESRION) { + TextSecurePreferences.setBlockingIdentityUpdates(getApplicationContext(), true); + } + return null; } diff --git a/src/org/thoughtcrime/securesms/MessageRecipientListItem.java b/src/org/thoughtcrime/securesms/MessageRecipientListItem.java index 48ea133fb7..49353a4f63 100644 --- a/src/org/thoughtcrime/securesms/MessageRecipientListItem.java +++ b/src/org/thoughtcrime/securesms/MessageRecipientListItem.java @@ -99,7 +99,7 @@ public class MessageRecipientListItem extends RelativeLayout resendButton.setVisibility(View.GONE); conflictButton.setVisibility(View.VISIBLE); - errorText = getContext().getString(R.string.MessageDetailsRecipient_new_identity); + errorText = getContext().getString(R.string.MessageDetailsRecipient_new_safety_numbers); conflictButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { diff --git a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java index 53a948e393..ce1d7491da 100644 --- a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java +++ b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java @@ -31,7 +31,6 @@ import org.thoughtcrime.securesms.color.MaterialColors; import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable; import org.thoughtcrime.securesms.crypto.MasterSecret; -import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.VibrateState; @@ -39,20 +38,15 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.preferences.AdvancedRingtonePreference; import org.thoughtcrime.securesms.preferences.ColorPreference; -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.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.DynamicTheme; +import org.thoughtcrime.securesms.util.IdentityUtil; 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.push.SignalServiceAddress; import java.util.concurrent.ExecutionException; @@ -296,7 +290,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi if (recipients.isBlocked()) blockPreference.setTitle(R.string.RecipientPreferenceActivity_unblock); else blockPreference.setTitle(R.string.RecipientPreferenceActivity_block); - getRemoteIdentityKey(getActivity(), masterSecret, recipients.getPrimaryRecipient()).addListener(new ListenableFuture.Listener>() { + IdentityUtil.getRemoteIdentityKey(getActivity(), masterSecret, recipients.getPrimaryRecipient()).addListener(new ListenableFuture.Listener>() { @Override public void onSuccess(Optional result) { if (result.isPresent()) { @@ -325,36 +319,6 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi }); } - private ListenableFuture> getRemoteIdentityKey(final Context context, - final MasterSecret masterSecret, - final Recipient recipient) - { - final SettableFuture> future = new SettableFuture<>(); - - 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()); - } - - @Override - protected void onPostExecute(Optional result) { - future.set(result); - } - }.execute(recipient); - - return future; - } - - private class RingtoneChangeListener implements Preference.OnPreferenceChangeListener { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { diff --git a/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java b/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java index 935d02d4da..b9e40a2971 100644 --- a/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java +++ b/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java @@ -51,6 +51,7 @@ 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.TextSecurePreferences; @@ -93,7 +94,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity @Override protected void onCreate(Bundle state, @NonNull MasterSecret masterSecret) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setTitle(R.string.AndroidManifest__verify_identity); + getSupportActionBar().setTitle(R.string.AndroidManifest__verify_safety_numbers); Recipient recipient = RecipientFactory.getRecipientForId(this, getIntent().getLongExtra(RECIPIENT_ID, -1), true); @@ -146,15 +147,16 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity .commit(); } - public static class VerifyDisplayFragment extends Fragment { + public static class VerifyDisplayFragment extends Fragment implements Recipients.RecipientsModifiedListener { 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"; - private String localNumber; - private String remoteNumber; + private Recipients recipient; + private String localNumber; + private String remoteNumber; private IdentityKey localIdentity; private IdentityKey remoteIdentity; @@ -164,6 +166,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity private View container; private ImageView qrCode; private ImageView qrVerified; + private TextView description; private View.OnClickListener clickListener; private TextView[] codes = new TextView[12]; @@ -172,21 +175,22 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity @Override public View onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) { - this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_display_fragment); - this.qrCode = ViewUtil.findById(container, R.id.qr_code); - this.qrVerified = ViewUtil.findById(container, R.id.qr_verified); - 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); - this.codes[3] = ViewUtil.findById(container, R.id.code_fourth); - this.codes[4] = ViewUtil.findById(container, R.id.code_fifth); - this.codes[5] = ViewUtil.findById(container, R.id.code_sixth); - this.codes[6] = ViewUtil.findById(container, R.id.code_seventh); - this.codes[7] = ViewUtil.findById(container, R.id.code_eighth); - this.codes[8] = ViewUtil.findById(container, R.id.code_ninth); - this.codes[9] = ViewUtil.findById(container, R.id.code_tenth); - this.codes[10] = ViewUtil.findById(container, R.id.code_eleventh); - this.codes[11] = ViewUtil.findById(container, R.id.code_twelth); + this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_display_fragment); + this.qrCode = ViewUtil.findById(container, R.id.qr_code); + this.qrVerified = ViewUtil.findById(container, R.id.qr_verified); + this.description = ViewUtil.findById(container, R.id.description); + 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); + this.codes[3] = ViewUtil.findById(container, R.id.code_fourth); + this.codes[4] = ViewUtil.findById(container, R.id.code_fifth); + this.codes[5] = ViewUtil.findById(container, R.id.code_sixth); + this.codes[6] = ViewUtil.findById(container, R.id.code_seventh); + this.codes[7] = ViewUtil.findById(container, R.id.code_eighth); + this.codes[8] = ViewUtil.findById(container, R.id.code_ninth); + this.codes[9] = ViewUtil.findById(container, R.id.code_tenth); + this.codes[10] = ViewUtil.findById(container, R.id.code_eleventh); + this.codes[11] = ViewUtil.findById(container, R.id.code_twelth); this.qrCode.setOnClickListener(clickListener); @@ -200,9 +204,17 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity 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.remoteIdentity = ((IdentityKeyParcelable)getArguments().getParcelable(REMOTE_IDENTITY)).get(); this.fingerprint = new NumericFingerprintGenerator(5200).createFor(localNumber, localIdentity, remoteNumber, remoteIdentity); + + this.recipient.addListener(this); + } + + @Override + public void onModified(Recipients recipients) { + setFingerprintViews(fingerprint); } @Override @@ -220,6 +232,12 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity } } + @Override + public void onDestroy() { + super.onDestroy(); + recipient.removeListener(this); + } + public void setScannedFingerprint(String scanned) { try { if (fingerprint.getScannableFingerprint().compareTo(scanned.getBytes("ISO-8859-1"))) { @@ -258,6 +276,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity Bitmap qrCodeBitmap = QrCode.create(qrCodeString); qrCode.setImageBitmap(qrCodeBitmap); + description.setText(getActivity().getString(R.string.verify_display_fragment__scan_the_code_on_your_contact_s_phone_or_ask_them_to_scan_your_code_to_verify_that_your_messages_are_end_to_end_encrypted_you_can_alternately_compare_the_number_above, recipient.toShortString())); } private Bitmap createVerifiedBitmap(int width, int height, @DrawableRes int id) { diff --git a/src/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java b/src/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java index a801806765..63b9e5a5a8 100644 --- a/src/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java +++ b/src/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java @@ -2,8 +2,10 @@ package org.thoughtcrime.securesms.crypto.storage; import android.content.Context; +import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.jobs.IdentityUpdateJob; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.IdentityKey; @@ -36,8 +38,22 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore { @Override public boolean isTrustedIdentity(String name, IdentityKey identityKey) { - long recipientId = RecipientFactory.getRecipientsFromString(context, name, true).getPrimaryRecipient().getRecipientId(); - return DatabaseFactory.getIdentityDatabase(context) - .isValidIdentity(recipientId, identityKey); + long recipientId = RecipientFactory.getRecipientsFromString(context, name, true).getPrimaryRecipient().getRecipientId(); + boolean trusted = DatabaseFactory.getIdentityDatabase(context) + .isValidIdentity(recipientId, identityKey); + + if (trusted) { + return true; + } else if (!TextSecurePreferences.isBlockingIdentityUpdates(context)) { + saveIdentity(name, identityKey); + + ApplicationContext.getInstance(context) + .getJobManager() + .add(new IdentityUpdateJob(context, recipientId)); + + return true; + } else { + return false; + } } } diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java index f421e28afb..2a303bf9b1 100644 --- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -489,7 +489,8 @@ public class SmsDatabase extends MessagingDatabase { type |= Types.END_SESSION_BIT; } - if (message.isPush()) type |= Types.PUSH_MESSAGE_BIT; + if (message.isPush()) type |= Types.PUSH_MESSAGE_BIT; + if (message.isIdentityUpdate()) type |= Types.KEY_EXCHANGE_IDENTITY_UPDATE_BIT; Recipients recipients; @@ -508,8 +509,9 @@ public class SmsDatabase extends MessagingDatabase { groupRecipients = RecipientFactory.getRecipientsFromString(context, message.getGroupId(), true); } - boolean unread = org.thoughtcrime.securesms.util.Util.isDefaultSmsProvider(context) || - message.isSecureMessage() || message.isGroup() || message.isPreKeyBundle(); + boolean unread = (org.thoughtcrime.securesms.util.Util.isDefaultSmsProvider(context) || + message.isSecureMessage() || message.isGroup() || message.isPreKeyBundle()) && + !message.isIdentityUpdate(); long threadId; @@ -542,7 +544,10 @@ public class SmsDatabase extends MessagingDatabase { DatabaseFactory.getThreadDatabase(context).setUnread(threadId); } - DatabaseFactory.getThreadDatabase(context).update(threadId, true); + if (!message.isIdentityUpdate()) { + DatabaseFactory.getThreadDatabase(context).update(threadId, true); + } + notifyConversationListeners(threadId); jobManager.add(new TrimThreadJob(context, threadId)); diff --git a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java index 82cfe16e42..d6c38bc94c 100644 --- a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -110,8 +110,10 @@ public abstract class MessageRecord extends DisplayRecord { return emphasisAdded(context.getString(R.string.MessageRecord_s_is_on_signal_say_hey, getIndividualRecipient().toShortString())); } else if (isExpirationTimerUpdate()) { String sender = isOutgoing() ? context.getString(R.string.MessageRecord_you) : getIndividualRecipient().toShortString(); - String time = ExpirationUtil.getExpirationDisplayValue(context, (int)(getExpiresIn() / 1000)); + String time = ExpirationUtil.getExpirationDisplayValue(context, (int) (getExpiresIn() / 1000)); return emphasisAdded(context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, sender, time)); + } else if (isIdentityUpdate()) { + return emphasisAdded(String.format("Your safety numbers with %s have changed", getIndividualRecipient().toShortString())); } else if (getBody().getBody().length() > MAX_DISPLAY_LENGTH) { return new SpannableString(getBody().getBody().substring(0, MAX_DISPLAY_LENGTH)); } diff --git a/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java index 1342abbe50..a8ca75b129 100644 --- a/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java @@ -75,9 +75,7 @@ public class SmsMessageRecord extends MessageRecord { } else if (MmsSmsColumns.Types.isLegacyType(type)) { return emphasisAdded(context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported)); } else if (isBundleKeyExchange()) { - return emphasisAdded(context.getString(R.string.SmsMessageRecord_received_message_with_unknown_identity_key_tap_to_process)); - } else if (isIdentityUpdate()) { - return emphasisAdded(context.getString(R.string.SmsMessageRecord_received_updated_but_unknown_identity_information)); + return emphasisAdded(context.getString(R.string.SmsMessageRecord_received_message_with_new_safety_numbers_tap_to_process)); } else if (isKeyExchange() && isOutgoing()) { return new SpannableString(""); } else if (isKeyExchange() && !isOutgoing()) { diff --git a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java index 0a950ac6e4..0ff9fbf8af 100644 --- a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -100,8 +100,10 @@ public class ThreadRecord extends DisplayRecord { } else if (SmsDatabase.Types.isJoinedType(type)) { return emphasisAdded(context.getString(R.string.ThreadRecord_s_is_on_signal_say_hey, getRecipients().getPrimaryRecipient().toShortString())); } else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) { - String time = ExpirationUtil.getExpirationDisplayValue(context, (int)(getExpiresIn() / 1000)); + String time = ExpirationUtil.getExpirationDisplayValue(context, (int) (getExpiresIn() / 1000)); return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time)); + } else if (SmsDatabase.Types.isIdentityUpdate(type)) { + return emphasisAdded(String.format("Your safety numbers with %s have changed", getRecipients().getPrimaryRecipient().toShortString())); } else { if (TextUtils.isEmpty(getBody().getBody())) { return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message))); diff --git a/src/org/thoughtcrime/securesms/jobs/IdentityUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/IdentityUpdateJob.java new file mode 100644 index 0000000000..8d05789974 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/IdentityUpdateJob.java @@ -0,0 +1,75 @@ +package org.thoughtcrime.securesms.jobs; + +import android.content.Context; + +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; +import org.thoughtcrime.securesms.database.SmsDatabase; +import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientFactory; +import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage; +import org.thoughtcrime.securesms.sms.IncomingTextMessage; +import org.whispersystems.jobqueue.JobParameters; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.messages.SignalServiceGroup; + +public class IdentityUpdateJob extends MasterSecretJob { + + private final long recipientId; + + public IdentityUpdateJob(Context context, long recipientId) { + super(context, JobParameters.newBuilder() + .withGroupId(IdentityUpdateJob.class.getName()) + .withPersistence() + .create()); + this.recipientId = recipientId; + } + + @Override + public void onRun(MasterSecret masterSecret) throws Exception { + Recipient recipient = RecipientFactory.getRecipientForId(context, recipientId, true); + Recipients recipients = RecipientFactory.getRecipientsFor(context, recipient, true); + String number = recipient.getNumber(); + long time = System.currentTimeMillis(); + SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context); + ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); + GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); + GroupDatabase.Reader reader = groupDatabase.getGroups(); + + GroupDatabase.GroupRecord groupRecord; + + while ((groupRecord = reader.getNext()) != null) { + if (groupRecord.getMembers().contains(number)) { + SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId()); + IncomingTextMessage incoming = new IncomingTextMessage(number, 1, time, null, Optional.of(group), 0); + IncomingIdentityUpdateMessage groupUpdate = new IncomingIdentityUpdateMessage(incoming); + + smsDatabase.insertMessageInbox(groupUpdate); + } + } + + if (threadDatabase.getThreadIdIfExistsFor(recipients) != -1) { + IncomingTextMessage incoming = new IncomingTextMessage(number, 1, time, null, Optional.absent(), 0); + IncomingIdentityUpdateMessage individualUpdate = new IncomingIdentityUpdateMessage(incoming); + smsDatabase.insertMessageInbox(individualUpdate); + } + } + + @Override + public boolean onShouldRetryThrowable(Exception exception) { + return false; + } + + @Override + public void onAdded() { + + } + + @Override + public void onCanceled() { + + } +} diff --git a/src/org/thoughtcrime/securesms/sms/IncomingIdentityUpdateMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingIdentityUpdateMessage.java new file mode 100644 index 0000000000..c1f1d79e19 --- /dev/null +++ b/src/org/thoughtcrime/securesms/sms/IncomingIdentityUpdateMessage.java @@ -0,0 +1,14 @@ +package org.thoughtcrime.securesms.sms; + +public class IncomingIdentityUpdateMessage extends IncomingTextMessage { + + public IncomingIdentityUpdateMessage(IncomingTextMessage base) { + super(base, ""); + } + + @Override + public boolean isIdentityUpdate() { + return true; + } + +} diff --git a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java index 86f5799b53..e030d9ac18 100644 --- a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java +++ b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java @@ -215,6 +215,10 @@ public class IncomingTextMessage implements Parcelable { return false; } + public boolean isIdentityUpdate() { + return false; + } + @Override public int describeContents() { return 0; diff --git a/src/org/thoughtcrime/securesms/util/IdentityUtil.java b/src/org/thoughtcrime/securesms/util/IdentityUtil.java new file mode 100644 index 0000000000..cb91a92d4a --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/IdentityUtil.java @@ -0,0 +1,51 @@ +package org.thoughtcrime.securesms.util; + +import android.content.Context; +import android.os.AsyncTask; +import android.support.annotation.UiThread; + +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore; +import org.thoughtcrime.securesms.recipients.Recipient; +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.push.SignalServiceAddress; + +public class IdentityUtil { + + @UiThread + public static ListenableFuture> getRemoteIdentityKey(final Context context, + final MasterSecret masterSecret, + final Recipient recipient) + { + final SettableFuture> future = new SettableFuture<>(); + + 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()); + } + + @Override + protected void onPostExecute(Optional result) { + future.set(result); + } + }.execute(recipient); + + return future; + } + +} diff --git a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java index 4b428d9fac..bdfc8f20ea 100644 --- a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -73,6 +73,7 @@ public class TextSecurePreferences { private static final String SIGNALING_KEY_PREF = "pref_signaling_key"; private static final String DIRECTORY_FRESH_TIME_PREF = "pref_directory_refresh_time"; private static final String IN_THREAD_NOTIFICATION_PREF = "pref_key_inthread_notifications"; + private static final String BLOCKING_IDENTITY_CHANGES_PREF = "pref_blocking_identity_changes"; private static final String LOCAL_REGISTRATION_ID_PREF = "pref_local_registration_id"; private static final String SIGNED_PREKEY_REGISTERED_PREF = "pref_signed_prekey_registered"; @@ -113,6 +114,14 @@ public class TextSecurePreferences { return getBooleanPreference(context, MULTI_DEVICE_PROVISIONED_PREF, false); } + public static boolean isBlockingIdentityUpdates(Context context) { + return getBooleanPreference(context, BLOCKING_IDENTITY_CHANGES_PREF, false); + } + + public static void setBlockingIdentityUpdates(Context context, boolean value) { + setBooleanPreference(context, BLOCKING_IDENTITY_CHANGES_PREF, value); + } + public static NotificationPrivacyPreference getNotificationPrivacy(Context context) { return new NotificationPrivacyPreference(getStringPreference(context, NOTIFICATION_PRIVACY_PREF, "all")); }