From a0ab252bc9b14b5c64fa432a2e1c15f2e0f35749 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 18 Jul 2018 08:27:05 -0700 Subject: [PATCH] Add preliminary contact discovery service support. --- build.gradle | 10 +- res/raw/ias.store | Bin 0 -> 1441 bytes .../securesms/push/IasTrustStore.java | 27 ++ .../push/SignalServiceNetworkAccess.java | 8 +- .../securesms/util/DirectoryHelper.java | 285 +++++++++++++++--- .../util/concurrent/SignalExecutors.java | 19 ++ 6 files changed, 296 insertions(+), 53 deletions(-) create mode 100644 res/raw/ias.store create mode 100644 src/org/thoughtcrime/securesms/push/IasTrustStore.java create mode 100644 src/org/thoughtcrime/securesms/util/concurrent/SignalExecutors.java diff --git a/build.gradle b/build.gradle index b5ec1d4ae1..b1b9194317 100644 --- a/build.gradle +++ b/build.gradle @@ -77,7 +77,7 @@ dependencies { compile 'com.google.android.exoplayer:exoplayer-core:2.8.4' compile 'com.google.android.exoplayer:exoplayer-ui:2.8.4' - compile 'org.whispersystems:signal-service-android:2.8.1' + compile 'org.whispersystems:signal-service-android:2.9.0' compile 'org.whispersystems:webrtc-android:M69' compile "me.leolin:ShortcutBadger:1.1.16" @@ -171,7 +171,7 @@ dependencyVerification { 'com.google.android.gms:play-services-maps:45e8021e7ddac4a44a82a0e9698991389ded3023d35c58f38dbd86d54211ec0e', 'com.google.android.exoplayer:exoplayer-ui:027557b2d69b15e1852a2530b36971f0dcc177abae240ee35e05f63502cdb0a7', 'com.google.android.exoplayer:exoplayer-core:e69b409e11887c955deb373357c30eeabf183395db0092b4817e0f80bb467d5b', - 'org.whispersystems:signal-service-android:414e91598abd941eb3be9a85702538cc9928d8c22f00e07716b83a096cbbe54d', + 'org.whispersystems:signal-service-android:bf469abcdcd2b2ba429024aca30713eaa3b83a77af34030a818b1c9fb4780044', 'org.whispersystems:webrtc-android:5493c92141ce884fc5ce8240d783232f4fe14bd17a8d0d7d1bd4944d0bd1682f', 'me.leolin:ShortcutBadger:e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774', 'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb', @@ -218,7 +218,7 @@ dependencyVerification { 'com.github.bumptech.glide:gifdecoder:59ccf3bb0cec11dab4b857382cbe0b171111b6fc62bf141adce4e1180889af15', 'com.android.support:support-annotations:af05330d997eb92a066534dbe0a3ea24347d26d7001221092113ae02a8f233da', 'org.whispersystems:signal-protocol-android:5b8acded7f2a40178eb90ab8e8cbfec89d170d91b3ff5e78487d1098df6185a1', - 'org.whispersystems:signal-service-java:c7ab92374e9656ba86a8d859cec71d03a68bba3e7ec0b7c597b726bf720eac21', + 'org.whispersystems:signal-service-java:4db9adf763071756cfd93fe48a40850f684ca02f2dea59601841abba7715c5c1', 'com.github.bumptech.glide:disklrucache:c1b1b6f5bbd01e2fcdc9d7f60913c8d338bdb65ed4a93bfa02b56f19daaade4b', 'com.github.bumptech.glide:annotations:bede99ef9f71517a4274bac18fd3e483e9f2b6108d7d6fe8f4949be4aa4d9512', 'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a', @@ -232,7 +232,9 @@ dependencyVerification { 'com.googlecode.libphonenumber:libphonenumber:183392c0565be16d3f6f86680b4106bbde6fe31a402ad21bf9823d938c0c8706', 'com.fasterxml.jackson.core:jackson-databind:835097bcdd11f5bc8a08378c70d4c8054dfa4b911691cc2752063c75534d198d', 'com.squareup.okhttp3:okhttp:7265adbd6f028aade307f58569d814835cd02bc9beffb70c25f72c9de50d61c4', + 'com.madgag.spongycastle:pkix:0d9cca6991f68eb373cfad309d5268c9fc38db5efb5fe00dcccf5c973af1eca1', 'com.madgag.spongycastle:prov:b8c3fec3a59aac1aa04ccf4dad7179351e54ef7672f53f508151b614c131398a', + 'org.threeten:threetenbp:f4c23ffaaed717c3b99c003e0ee02d6d66377fd47d866fec7d971bd8644fc1a7', 'org.whispersystems:curve25519-java:7dd659d8822c06c3aea1a47f18fac9e5761e29cab8100030b877db445005f03e', 'com.fasterxml.jackson.core:jackson-annotations:0ca408c24202a7626ec8b861e99d85eca5e38b73311dd6dd12e3e9deecc3fe94', 'com.fasterxml.jackson.core:jackson-core:cbf4604784b4de226262845447a1ad3bb38a6728cebe86562e2c5afada8be2c0', @@ -265,11 +267,13 @@ android { buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L" buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\"" buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\"" + buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\"" buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\"" buildConfigField "String", "GIPHY_PROXY_HOST", "\"giphy-proxy-production.whispersystems.org\"" buildConfigField "int", "GIPHY_PROXY_PORT", "80" buildConfigField "String", "USER_AGENT", "\"OWA\"" buildConfigField "boolean", "DEV_BUILD", "false" + buildConfigField "String", "MRENCLAVE", "\"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9\"" ndk { abiFilters "armeabi", "armeabi-v7a", "x86" diff --git a/res/raw/ias.store b/res/raw/ias.store new file mode 100644 index 0000000000000000000000000000000000000000..e0b8ec8ccacc48459adb2384402b887682cf45da GIT binary patch literal 1441 zcmZQzU|?ckU=WcLo>6|#P&tUfG}q(0mH|~V zFtA4GnHpFEX@7$zR&RqQ=FJP3nHZUvI2kUom&F<_-Y~_0myJ`a&7dghg+ z<|sJl7Zv0eC6;97=NTFp=z-*zdDJ0t!R`?XjwL0j#U)^Qg`iZR(h`N>%=Em>ymSR; zM*{_MUPCiOLqiioQ)6>u!zc-UBLfplLnC7#GPN|dj2dtkHZd+j4m3tq2IeNleg=ak z#xABN#zuzuHYs&m%WZZC>Z%(Zxb7IYJ9NRXdH3{ECeD1;B)e1Ks$sb~XSrKeR-Aj= znM-$HUlEy`rJt-CH1{IU$#(M%<#VLiAI))JxQ@Zo!#&4izU$J3u~jGD99whDb?p}0 z_Vl@5=WtpdUGy?DC!``odiuOl?{G_zeW922zZ{r*UtMvg$=R8#>YIP+UGLwosgkwt zbfF60=Q+BC>|5MJqUwJOyk_IdSlQa3IL~Y%XHbU4q-#qbt>9pmv~wsmbnr9VESh9K zb5&D7iNo}eMfYEPU4QjovYh;(^%fix@->!ExOsQ}gM`Vmg~BHsZHQ&A{hBp1SbEyM z%zMUfm2wU$)CxFlJ-NEx!T-46fq(9qzo%Agyu)`S^Ocn9ga0!{I`Tec&Q^}z(o%cv zzr!}RXX5OW5B;98StO!<&R^Byr2DTna$8@zSysGo$zGQu!K=R1AN+Lr!snLnUTi<+ zaLngjc6R%pcP~yhN%e(%ieVAE*yGkbE51SK_g&lLzN$%;YY!c`8udS)P01-%nu(c_ zfpKx;NrT2?1_{8_Co9hqX%KD@x*&K#V4HtNNlAf~zJ5tjX>mzvN^xpYS!Qx-v0f%9 z>FOot=jtct1DSax`p!W<`oQF>kCa>?sTG=7ogMX(i*gKPK{oKQh_Q%NI9Hb~tJ`y7 zeVvo}YttI*cE&&(19_0NGK++PSOazid>{q9Px zoDrv>YgGS16DLEx@@1A&Zcd+i;+1Pzlg+EHiw`HBUy<}{$4aBiH#Y=Fu`^u=jW2ns z@R-@)vq{7AwI{EikW(lyblT5wRAU9Vn{ngc1g)my%t4K-o=#0Vv(?yd#RR)lZbr7o zujl%<>8+XL=-HuS^=8ti=#Mt{zrQ}C+4od>SCpy6wrsW2N1_7nG1#m%NHttr^YzMm z{tniQJB4jkgrhbeNs-d;;4qn6Y^)i_SN6{Ps8W$pQs1wJ>vJcq_OlgK`20KSqU71$ z5U+(-mbJxYhwobxUT9FKXS0)?cS*JPpN`B2BEqjYO RcZ)scQZTsQY7d+&%4M^ zza6~JavyCuJ*PQ@rPznB?pQ+1{qy=?EVo@LoL`{N$78?1|F+Pw(^?l-%gp-t1Bm$#Naxay6{qM3DBmjOA4C9ePg literal 0 HcmV?d00001 diff --git a/src/org/thoughtcrime/securesms/push/IasTrustStore.java b/src/org/thoughtcrime/securesms/push/IasTrustStore.java new file mode 100644 index 0000000000..e06d2623db --- /dev/null +++ b/src/org/thoughtcrime/securesms/push/IasTrustStore.java @@ -0,0 +1,27 @@ +package org.thoughtcrime.securesms.push; + +import android.content.Context; + +import org.thoughtcrime.securesms.R; +import org.whispersystems.signalservice.api.push.TrustStore; + +import java.io.InputStream; + +public class IasTrustStore implements TrustStore { + + private final Context context; + + public IasTrustStore(Context context) { + this.context = context.getApplicationContext(); + } + + @Override + public InputStream getKeyStoreInputStream() { + return context.getResources().openRawResource(R.raw.ias); + } + + @Override + public String getKeyStorePassword() { + return "whisper"; + } +} diff --git a/src/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java b/src/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java index 15c3fd573c..9c319fae46 100644 --- a/src/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java +++ b/src/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java @@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; +import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; @@ -54,8 +55,10 @@ public class SignalServiceNetworkAccess { final TrustStore trustStore = new DomainFrontingTrustStore(context); final SignalServiceUrl service = new SignalServiceUrl("https://cms.souqcdn.com", SERVICE_REFLECTOR_HOST, trustStore, SOUQ_CONNECTION_SPEC); final SignalCdnUrl serviceCdn = new SignalCdnUrl("https://cms.souqcdn.com", SERVICE_REFLECTOR_HOST, trustStore, SOUQ_CONNECTION_SPEC); + final SignalContactDiscoveryUrl serviceContact = new SignalContactDiscoveryUrl("https://cms.souqcdn.com", SERVICE_REFLECTOR_HOST, trustStore, SOUQ_CONNECTION_SPEC); final SignalServiceConfiguration serviceConfig = new SignalServiceConfiguration(new SignalServiceUrl[] { service }, - new SignalCdnUrl[] { serviceCdn }); + new SignalCdnUrl[] { serviceCdn }, + new SignalContactDiscoveryUrl[] { serviceContact }); this.censorshipConfiguration = new HashMap() {{ put(COUNTRY_CODE_EGYPT, serviceConfig); @@ -65,7 +68,8 @@ public class SignalServiceNetworkAccess { }}; this.uncensoredConfiguration = new SignalServiceConfiguration(new SignalServiceUrl[] {new SignalServiceUrl(BuildConfig.SIGNAL_URL, new SignalServiceTrustStore(context))}, - new SignalCdnUrl[] {new SignalCdnUrl(BuildConfig.SIGNAL_CDN_URL, new SignalServiceTrustStore(context))}); + new SignalCdnUrl[] {new SignalCdnUrl(BuildConfig.SIGNAL_CDN_URL, new SignalServiceTrustStore(context))}, + new SignalContactDiscoveryUrl[] {new SignalContactDiscoveryUrl(BuildConfig.SIGNAL_CONTACT_DISCOVERY_URL, new SignalServiceTrustStore(context))}); this.censoredCountries = this.censorshipConfiguration.keySet().toArray(new String[0]); } diff --git a/src/org/thoughtcrime/securesms/util/DirectoryHelper.java b/src/org/thoughtcrime/securesms/util/DirectoryHelper.java index e60cbebf21..fed2f4e49c 100644 --- a/src/org/thoughtcrime/securesms/util/DirectoryHelper.java +++ b/src/org/thoughtcrime/securesms/util/DirectoryHelper.java @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.util; import android.Manifest; import android.accounts.Account; import android.accounts.AccountManager; +import android.annotation.SuppressLint; import android.content.ContentResolver; import android.content.Context; import android.content.OperationApplicationException; @@ -18,6 +19,7 @@ import com.annimon.stream.Collectors; import com.annimon.stream.Stream; import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.crypto.SessionUtil; @@ -31,24 +33,41 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.push.AccountManagerFactory; +import org.thoughtcrime.securesms.push.IasTrustStore; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.sms.IncomingJoinedMessage; +import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.push.ContactTokenDetails; +import org.whispersystems.signalservice.api.push.TrustStore; +import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; +import org.whispersystems.signalservice.internal.contacts.crypto.Quote; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; public class DirectoryHelper { private static final String TAG = DirectoryHelper.class.getSimpleName(); + private static final int CONTACT_DISCOVERY_BATCH_SIZE = 2048; + public static void refreshDirectory(@NonNull Context context, boolean notifyOfNewUsers) throws IOException { @@ -66,15 +85,16 @@ public class DirectoryHelper { if (notifyOfNewUsers) notifyNewUsers(context, newlyActiveUsers); } + @SuppressLint("CheckResult") private static @NonNull List
refreshDirectory(@NonNull Context context, @NonNull SignalServiceAccountManager accountManager) throws IOException { if (TextUtils.isEmpty(TextSecurePreferences.getLocalNumber(context))) { - return new LinkedList<>(); + return Collections.emptyList(); } if (!Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) { - return new LinkedList<>(); + return Collections.emptyList(); } RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); @@ -82,42 +102,33 @@ public class DirectoryHelper { Stream eligibleSystemDatabaseContactNumbers = Stream.of(ContactAccessor.getInstance().getAllContactsWithNumbers(context)).map(Address::serialize); Set eligibleContactNumbers = Stream.concat(eligibleRecipientDatabaseContactNumbers, eligibleSystemDatabaseContactNumbers).collect(Collectors.toSet()); - List activeTokens = accountManager.getContacts(eligibleContactNumbers); - - if (activeTokens != null) { - List
activeAddresses = new LinkedList<>(); - List
inactiveAddresses = new LinkedList<>(); + Future legacyRequest = getLegacyDirectoryResult(context, accountManager, recipientDatabase, eligibleContactNumbers); + List>> contactServiceRequest = getContactServiceDirectoryResult(context, accountManager, eligibleContactNumbers); - Set inactiveContactNumbers = new HashSet<>(eligibleContactNumbers); + try { + DirectoryResult legacyResult = legacyRequest.get(); + Set contactServiceResult = executeAndMergeContactDiscoveryRequests(accountManager, contactServiceRequest); - for (ContactTokenDetails activeToken : activeTokens) { - activeAddresses.add(Address.fromSerialized(activeToken.getNumber())); - inactiveContactNumbers.remove(activeToken.getNumber()); - } - - for (String inactiveContactNumber : inactiveContactNumbers) { - inactiveAddresses.add(Address.fromSerialized(inactiveContactNumber)); + if (legacyResult.getNumbers().size() == contactServiceResult.size() && legacyResult.getNumbers().containsAll(contactServiceResult)) { + Log.i(TAG, "[Batch] New contact discovery service request matched existing results."); + accountManager.reportContactDiscoveryServiceMatch(); + } else { + Log.w(TAG, "[Batch] New contact discovery service request did NOT match existing results."); + accountManager.reportContactDiscoveryServiceMismatch(); } - Set
currentActiveAddresses = new HashSet<>(recipientDatabase.getRegistered()); - Set
contactAddresses = new HashSet<>(recipientDatabase.getSystemContacts()); - List
newlyActiveAddresses = Stream.of(activeAddresses) - .filter(address -> !currentActiveAddresses.contains(address)) - .filter(contactAddresses::contains) - .toList(); + return legacyResult.getNewlyActiveAddresses(); - recipientDatabase.setRegistered(activeAddresses, inactiveAddresses); - updateContactsDatabase(context, activeAddresses, true); - - if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context)) { - return newlyActiveAddresses; + } catch (InterruptedException e) { + throw new IOException("[Batch] Operation was interrupted.", e); + } catch (ExecutionException e) { + if (e.getCause() instanceof IOException) { + throw (IOException) e.getCause(); } else { - TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true); - return new LinkedList<>(); + Log.e(TAG, "[Batch] Experienced an unexpected exception.", e); + throw new AssertionError(e); } } - - return new LinkedList<>(); } public static RegisteredState refreshDirectoryFor(@NonNull Context context, @@ -126,30 +137,34 @@ public class DirectoryHelper { { RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); SignalServiceAccountManager accountManager = AccountManagerFactory.createManager(context); - boolean activeUser = recipient.resolve().getRegistered() == RegisteredState.REGISTERED; - boolean systemContact = recipient.isSystemContact(); - String number = recipient.getAddress().serialize(); - Optional details = accountManager.getContact(number); - if (details.isPresent()) { - recipientDatabase.setRegistered(recipient, RegisteredState.REGISTERED); + Future legacyRequest = getLegacyRegisteredState(context, accountManager, recipientDatabase, recipient); + List>> contactServiceRequest = getContactServiceDirectoryResult(context, accountManager, Collections.singleton(recipient.getAddress().serialize())); - if (Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) { - updateContactsDatabase(context, Util.asList(recipient.getAddress()), false); - } + try { + RegisteredState legacyState = legacyRequest.get(); + Set contactServiceResult = executeAndMergeContactDiscoveryRequests(accountManager, contactServiceRequest); + RegisteredState contactServiceState = contactServiceResult.size() == 1 ? RegisteredState.REGISTERED : RegisteredState.NOT_REGISTERED; - if (!activeUser && TextSecurePreferences.isMultiDevice(context)) { - ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceContactUpdateJob(context)); + if (legacyState == contactServiceState) { + Log.i(TAG, "[Singular] New contact discovery service request matched existing results."); + accountManager.reportContactDiscoveryServiceMatch(); + } else { + Log.w(TAG, "[Singular] New contact discovery service request did NOT match existing results."); + accountManager.reportContactDiscoveryServiceMismatch(); } - if (!activeUser && systemContact && !TextSecurePreferences.getNeedsSqlCipherMigration(context)) { - notifyNewUsers(context, Collections.singletonList(recipient.getAddress())); - } + return legacyState; - return RegisteredState.REGISTERED; - } else { - recipientDatabase.setRegistered(recipient, RegisteredState.NOT_REGISTERED); - return RegisteredState.NOT_REGISTERED; + } catch (InterruptedException e) { + throw new IOException("[Singular] Operation was interrupted.", e); + } catch (ExecutionException e) { + if (e.getCause() instanceof IOException) { + throw (IOException) e.getCause(); + } else { + Log.e(TAG, "[Singular] Experienced an unexpected exception.", e); + throw new AssertionError(e); + } } } @@ -249,6 +264,180 @@ public class DirectoryHelper { } } + private static Future getLegacyDirectoryResult(@NonNull Context context, + @NonNull SignalServiceAccountManager accountManager, + @NonNull RecipientDatabase recipientDatabase, + @NonNull Set eligibleContactNumbers) + { + return SignalExecutors.IO.submit(() -> { + List activeTokens = accountManager.getContacts(eligibleContactNumbers); + + if (activeTokens != null) { + List
activeAddresses = new LinkedList<>(); + List
inactiveAddresses = new LinkedList<>(); + + Set inactiveContactNumbers = new HashSet<>(eligibleContactNumbers); + + for (ContactTokenDetails activeToken : activeTokens) { + activeAddresses.add(Address.fromSerialized(activeToken.getNumber())); + inactiveContactNumbers.remove(activeToken.getNumber()); + } + + for (String inactiveContactNumber : inactiveContactNumbers) { + inactiveAddresses.add(Address.fromSerialized(inactiveContactNumber)); + } + + Set
currentActiveAddresses = new HashSet<>(recipientDatabase.getRegistered()); + Set
contactAddresses = new HashSet<>(recipientDatabase.getSystemContacts()); + List
newlyActiveAddresses = Stream.of(activeAddresses) + .filter(address -> !currentActiveAddresses.contains(address)) + .filter(contactAddresses::contains) + .toList(); + + recipientDatabase.setRegistered(activeAddresses, inactiveAddresses); + updateContactsDatabase(context, activeAddresses, true); + + Set activeContactNumbers = Stream.of(activeAddresses).map(Address::serialize).collect(Collectors.toSet()); + + if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context)) { + return new DirectoryResult(activeContactNumbers, newlyActiveAddresses); + } else { + TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true); + return new DirectoryResult(activeContactNumbers); + } + } + return new DirectoryResult(Collections.emptySet(), Collections.emptyList()); + }); + } + + private static Future getLegacyRegisteredState(@NonNull Context context, + @NonNull SignalServiceAccountManager accountManager, + @NonNull RecipientDatabase recipientDatabase, + @NonNull Recipient recipient) + { + return SignalExecutors.IO.submit(() -> { + boolean activeUser = recipient.resolve().getRegistered() == RegisteredState.REGISTERED; + boolean systemContact = recipient.isSystemContact(); + String number = recipient.getAddress().serialize(); + Optional details = accountManager.getContact(number); + + if (details.isPresent()) { + recipientDatabase.setRegistered(recipient, RegisteredState.REGISTERED); + + if (Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) { + updateContactsDatabase(context, Util.asList(recipient.getAddress()), false); + } + + if (!activeUser && TextSecurePreferences.isMultiDevice(context)) { + ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceContactUpdateJob(context)); + } + + if (!activeUser && systemContact && !TextSecurePreferences.getNeedsSqlCipherMigration(context)) { + notifyNewUsers(context, Collections.singletonList(recipient.getAddress())); + } + + return RegisteredState.REGISTERED; + } else { + recipientDatabase.setRegistered(recipient, RegisteredState.NOT_REGISTERED); + return RegisteredState.NOT_REGISTERED; + } + }); + } + + private static List>> getContactServiceDirectoryResult(@NonNull Context context, + @NonNull SignalServiceAccountManager accountManager, + @NonNull Set eligibleContactNumbers) + { + List> batches = splitIntoBatches(eligibleContactNumbers, CONTACT_DISCOVERY_BATCH_SIZE); + List>> futures = new ArrayList<>(batches.size()); + + for (Set batch : batches) { + Future> future = SignalExecutors.IO.submit(() -> { + return new HashSet<>(accountManager.getRegisteredUsers(getIasKeyStore(context), batch, BuildConfig.MRENCLAVE)); + }); + futures.add(future); + } + return futures; + } + + private static List> splitIntoBatches(@NonNull Set numbers, int batchSize) { + List numberList = new ArrayList<>(numbers); + List> batches = new LinkedList<>(); + + for (int i = 0; i < numberList.size(); i += batchSize) { + List batch = numberList.subList(i, Math.min(numberList.size(), i + batchSize)); + batches.add(new HashSet<>(batch)); + } + + return batches; + } + + private static Set executeAndMergeContactDiscoveryRequests(@NonNull SignalServiceAccountManager accountManager, @NonNull List>> futures) { + Set results = new HashSet<>(); + for (Future> future : futures) { + try { + results.addAll(future.get()); + } catch (InterruptedException e) { + Log.w(TAG, "Contact discovery batch was interrupted.", e); + accountManager.reportContactDiscoveryServiceUnexpectedError(); + } catch (ExecutionException e) { + if (isAttestationError(e.getCause())) { + Log.w(TAG, "Failed during attestation.", e); + accountManager.reportContactDiscoveryServiceAttestationError(); + } else if (e.getCause() instanceof PushNetworkException) { + Log.w(TAG, "Failed due to poor network.", e); + } else { + Log.w(TAG, "Failed for an unknown reason.", e); + accountManager.reportContactDiscoveryServiceUnexpectedError(); + } + } + } + + return results; + } + + private static boolean isAttestationError(Throwable e) { + return e instanceof CertificateException || + e instanceof SignatureException || + e instanceof UnauthenticatedQuoteException || + e instanceof UnauthenticatedResponseException || + e instanceof Quote.InvalidQuoteFormatException; + } + + private static KeyStore getIasKeyStore(@NonNull Context context) + throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException + { + TrustStore contactTrustStore = new IasTrustStore(context); + + KeyStore keyStore = KeyStore.getInstance("BKS"); + keyStore.load(contactTrustStore.getKeyStoreInputStream(), contactTrustStore.getKeyStorePassword().toCharArray()); + + return keyStore; + } + + private static class DirectoryResult { + + private final Set numbers; + private final List
newlyActiveAddresses; + + DirectoryResult(@NonNull Set numbers) { + this(numbers, Collections.emptyList()); + } + + DirectoryResult(@NonNull Set numbers, @NonNull List
newlyActiveAddresses) { + this.numbers = numbers; + this.newlyActiveAddresses = newlyActiveAddresses; + } + + Set getNumbers() { + return numbers; + } + + List
getNewlyActiveAddresses() { + return newlyActiveAddresses; + } + } + private static class AccountHolder { private final boolean fresh; diff --git a/src/org/thoughtcrime/securesms/util/concurrent/SignalExecutors.java b/src/org/thoughtcrime/securesms/util/concurrent/SignalExecutors.java new file mode 100644 index 0000000000..e9a756a30a --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/concurrent/SignalExecutors.java @@ -0,0 +1,19 @@ +package org.thoughtcrime.securesms.util.concurrent; + +import android.support.annotation.NonNull; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +public class SignalExecutors { + + public static final ExecutorService IO = Executors.newCachedThreadPool(new ThreadFactory() { + private final AtomicInteger counter = new AtomicInteger(); + @Override + public Thread newThread(@NonNull Runnable r) { + return new Thread(r, "signal-io-" + counter.getAndIncrement()); + } + }); +}