From 848a25664b746f89b4435b01728247bf3d3135f4 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Mon, 29 Aug 2016 17:49:49 -0700 Subject: [PATCH] Make fingerprint changes optionally non-blocking Also complete the rename from "identity" to "safety numbers." // FREEBIE --- build.gradle | 5 +- gradle/wrapper/gradle-wrapper.properties | 4 +- libs/gradle-witness.jar | Bin 20436 -> 16693 bytes res/drawable-hdpi/ic_security_white_24dp.png | Bin 0 -> 428 bytes res/drawable-mdpi/ic_security_white_24dp.png | Bin 0 -> 288 bytes res/drawable-xhdpi/ic_security_white_24dp.png | Bin 0 -> 507 bytes .../ic_security_white_24dp.png | Bin 0 -> 702 bytes .../ic_security_white_24dp.png | Bin 0 -> 913 bytes res/layout/verify_display_fragment.xml | 3 +- res/values/strings.xml | 35 +++----- res/xml/preferences_app_protection.xml | 6 ++ res/xml/recipient_preferences.xml | 2 +- .../securesms/ConversationAdapter.java | 6 +- .../securesms/ConversationFragment.java | 2 +- .../securesms/ConversationUpdateItem.java | 83 ++++++++++++++---- .../securesms/DatabaseUpgradeActivity.java | 7 ++ .../securesms/MessageRecipientListItem.java | 2 +- .../RecipientPreferenceActivity.java | 40 +-------- .../securesms/VerifyIdentityActivity.java | 57 ++++++++---- .../storage/TextSecureIdentityKeyStore.java | 22 ++++- .../securesms/database/SmsDatabase.java | 13 ++- .../database/model/MessageRecord.java | 4 +- .../database/model/SmsMessageRecord.java | 4 +- .../database/model/ThreadRecord.java | 4 +- .../securesms/jobs/IdentityUpdateJob.java | 75 ++++++++++++++++ .../sms/IncomingIdentityUpdateMessage.java | 14 +++ .../securesms/sms/IncomingTextMessage.java | 4 + .../securesms/util/IdentityUtil.java | 51 +++++++++++ .../securesms/util/TextSecurePreferences.java | 9 ++ 29 files changed, 331 insertions(+), 121 deletions(-) create mode 100644 res/drawable-hdpi/ic_security_white_24dp.png create mode 100644 res/drawable-mdpi/ic_security_white_24dp.png create mode 100644 res/drawable-xhdpi/ic_security_white_24dp.png create mode 100644 res/drawable-xxhdpi/ic_security_white_24dp.png create mode 100644 res/drawable-xxxhdpi/ic_security_white_24dp.png create mode 100644 src/org/thoughtcrime/securesms/jobs/IdentityUpdateJob.java create mode 100644 src/org/thoughtcrime/securesms/sms/IncomingIdentityUpdateMessage.java create mode 100644 src/org/thoughtcrime/securesms/util/IdentityUtil.java 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 561041d3661895d48bd396b342d9c043ecd741ae..6ab83e596258e069a075bcee5d353177160aae2c 100644 GIT binary patch literal 16693 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 Az5oCK literal 20436 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 OSn7eTVFr-R!tx*AKLw-! 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 0000000000000000000000000000000000000000..262800a4d89c11f90c2607e2596b00cb8c917f6d GIT binary patch literal 428 zcmV;d0aN~oP)4fI5C-6YO+fBAn}8q+78XVk5+sONUl)m_IRvrD9}u(=3HJkR6@sJ@n^;)cg!GDH znM#W&R@X?d@DAP0BiIb&yWQJw3qkOma^NYZn3=i17=3+nv#P#5Z^J&=w_&~=v|wl? z!pP7<9<*rN1i*KZiEWF`puSy^1NU?t&u%)kbRv;A)}8p%Ns~-1nAMnw!j2WutIEh~ z3Jn2=9y=RIEyS=2+R+K1Jrs@q(7yp~wt_ZO=s^usNb3mPPocXiGZlXd?xfI-Oz17R zo?&};u_W!7(x W*Q%T(FN`3Qp#N3mm6la}KEbFt(9` za6c2A<4#C{x?(EWH8!%9gK(Au+XiS0Ik4n51)7GqQJN0IifaHndf>{kNJ09McK`zi mB0c1{_!j` z(65rV@qh=MM2Lnc{VF;T-}p_AA+fQ@0*E#)O97&dFAWf6S-=Wd0V}{;2=GG(zFGh~ z`e6F310P6Frw?J8>A({w;1s4uI&i}YxQgkz4qR{oo?$wt14kWz3O=T~4hU~@0~ay9 zMv{PTPN2XP)1_qK2!1YL1Cz($WI(u=1K7nB(*Q{WS_GMa%a{V1>40!IBe2E}rrU@O zP~%$$V1KlpFI4RSVbdBY@szOhh%F!ttbn~dByg;HXZ-~002ovPDHLkV1k+$*vtR` literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7bcb2fd013f9a7fef71171f0852ddf109135987d GIT binary patch literal 702 zcmV;v0zv(WP)4uW0Wf`YiI z1yL{#LUF95wDo}?CDfvAO_Thc%J~v$J-LrBpx^(_4Zo1n<(?b+4~rr}KY8->lOSe$ zY(JolL1tKOhC$kz2TC)_0`)C0N;-T{FFDGrRhb;U;eb{rMv>W6nM{l#^Qp3%80jsN z)l}I`jLb_PF(j+0QcaBJySthy#l+~Q!_`zNCPw*96=|kQF)_+nP^9^!Mn3vGBkTOP zn575lfBEo1c}~j2G#ezpbMry+2N5U&MW6^2fg(@@`o};kp@B*|(0pj1q7L-M2(+v| z(kC70Jtk@4+mI%7peJ5I8KlQL&@Hc^OGsHAXuvBdhjc~<>hTDQ^A)LE2THQ!4RjFc z2MO(E!?-ulHKb?y+mAs{pg2=V7xkb$oztP$w0nbB%%8 zS#}N@Lt13B4kYKkW6%LANOv2*^xQ?+Dd+{#3TZt^&O?WwQv@3`yB6Nbq9afTMWml} zm|mP;aRN&4l3+vTH~E_=H~`%y*d!ZmRttSpEJ5c9R_3tn@@3cxbebx`uG?>el8hUJ z2B;A1ISCi*sjW;KfZ|-GwgT<0_GMknH3-_mQ)>IhK9?KC9_IBRPVkx9igdePaUP+l z0`2Az_04g}#}aryvws0~@qiNbO|#e6RyxHS*6J*;sPbp8+3s_1zkw|Js@x>$|7F8* zrqER2SXfUjS{Y`U^;+Tvt>HdK*}*+ZNYXFvkZR6zofJcSB-kvMWr)KB$!Q_O2p^av kONN&4A2j(q5lC@<1G;jC0F4+6x&QzG07*qoM6N<$g6>#9O#lD@ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b1eddbd6c303b0e0b43d9b6a39ff77a2be6e4f52 GIT binary patch literal 913 zcmV;C18)3@P)YC}3+H>*gM8q5z{NZxT&8llsArT7 zHW;NIVd@FYycDzS@o#&~l1dIB%^W9qJ7JD=`~ewOIp?y@StS!!z{SL0C#_qdb>bD$ zwiWs&UOg>aVRGW-3~hz!i9ayC6(%Qsn;Du-Z-uFe|B4an5D3#-p?~7bEc4)hpEA7_ zx+nfEx)9Ryr*$jzO?-}Nnh^*EfQGHmGx1;Y-!w0-c z=anO&1$p9=2aqQ|fdJtE4&VR|-~bNb01n^)4&VR| z-~bT;DzO4o)B%6T3UI0pD8>pr4n-w~qF@DAiQ)B)>O0goZ~nL6Nx zRlrNgy{!(Iu?lz}x#!gZ16Bds$UUMCXtM~o9l0tgb$}pm4e%6lIYbb^k~P35$h{W~ z@QfuuBS*+h1Os$i0*v@Jpd%PS@Pie=8giS6pnx|l0B)m%+-t!B_gVnFinn`$0|c89 z18(Lw0cp-60qa-0D}8C)&$5+#u?TV99rE_9RJsk^xC}SyOoa!(+Vy2Zz(4^BuGWZ<8lnz#pkHDMrXXvafkVd z>$b&U91o%!$g;;({mc~g@xAD7|5}fDmrRm=)j-h1FbnLk n!vaGzA - 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")); }