diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt index 1924f85fba..11e40899de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt @@ -69,7 +69,7 @@ object ContactDiscovery { context = context, descriptor = "refresh-all", refresh = { - DirectoryHelper.refreshAll(context) + ContactDiscoveryRefreshV1.refreshAll(context) }, removeSystemContactLinksIfMissing = true, notifyOfNewUsers = notifyOfNewUsers @@ -86,7 +86,7 @@ object ContactDiscovery { context = context, descriptor = "refresh-multiple", refresh = { - DirectoryHelper.refresh(context, recipients) + ContactDiscoveryRefreshV1.refresh(context, recipients) }, removeSystemContactLinksIfMissing = false, notifyOfNewUsers = notifyOfNewUsers @@ -101,7 +101,7 @@ object ContactDiscovery { context = context, descriptor = "refresh-single", refresh = { - DirectoryHelper.refresh(context, listOf(recipient)) + ContactDiscoveryRefreshV1.refresh(context, listOf(recipient)) }, removeSystemContactLinksIfMissing = false, notifyOfNewUsers = notifyOfNewUsers diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryHsmV1.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryHsmV1.java deleted file mode 100644 index bdb294bb6c..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryHsmV1.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.thoughtcrime.securesms.contacts.sync; - -import androidx.annotation.NonNull; -import androidx.annotation.WorkerThread; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.BuildConfig; -import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper.DirectoryResult; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.signal.core.util.SetUtil; -import org.whispersystems.signalservice.api.SignalServiceAccountManager; -import org.whispersystems.signalservice.api.push.ACI; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Uses CDSHv1 to map E164's to UUIDs. - */ -class ContactDiscoveryHsmV1 { - - private static final String TAG = Log.tag(ContactDiscoveryHsmV1.class); - - private static final int MAX_NUMBERS = 20_500; - - @WorkerThread - static DirectoryResult getDirectoryResult(@NonNull Set databaseNumbers, @NonNull Set systemNumbers) throws IOException { - Set allNumbers = SetUtil.union(databaseNumbers, systemNumbers); - FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(allNumbers, databaseNumbers); - Set sanitizedNumbers = sanitizeNumbers(inputResult.getNumbers()); - Set ignoredNumbers = new HashSet<>(); - - if (sanitizedNumbers.size() > MAX_NUMBERS) { - Set randomlySelected = randomlySelect(sanitizedNumbers, MAX_NUMBERS); - - ignoredNumbers = SetUtil.difference(sanitizedNumbers, randomlySelected); - sanitizedNumbers = randomlySelected; - } - - SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); - - try { - Map results = accountManager.getRegisteredUsersWithCdshV1(sanitizedNumbers, BuildConfig.CDSH_PUBLIC_KEY, BuildConfig.CDSH_CODE_HASH); - FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(results, inputResult); - - return new DirectoryResult(outputResult.getNumbers(), outputResult.getRewrites(), ignoredNumbers); - } catch (IOException e) { - Log.w(TAG, "Attestation error.", e); - throw new IOException(e); - } - } - - private static Set sanitizeNumbers(@NonNull Set numbers) { - return numbers.stream().filter(number -> { - try { - return number.startsWith("+") && number.length() > 1 && number.charAt(1) != '0' && Long.parseLong(number.substring(1)) > 0; - } catch (NumberFormatException e) { - return false; - } - }).collect(Collectors.toSet()); - } - - private static @NonNull Set randomlySelect(@NonNull Set numbers, int max) { - List list = new ArrayList<>(numbers); - Collections.shuffle(list); - - return new HashSet<>(list.subList(0, max)); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV1.java similarity index 66% rename from app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java rename to app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV1.java index d93de7aa3f..df28acaa0a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV1.java @@ -10,7 +10,9 @@ import com.annimon.stream.Stream; import org.signal.contacts.SystemContactsRepository; import org.signal.core.util.logging.Log; +import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.util.Pair; +import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery.RefreshResult; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.SignalDatabase; @@ -19,6 +21,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; +import org.thoughtcrime.securesms.push.IasTrustStore; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.FeatureFlags; @@ -26,13 +29,24 @@ import org.thoughtcrime.securesms.util.ProfileUtil; import org.signal.core.util.SetUtil; import org.thoughtcrime.securesms.util.Stopwatch; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.ACI; +import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.api.services.ProfileService; import org.whispersystems.signalservice.internal.ServiceResponse; +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.Collections; import java.util.HashSet; import java.util.List; @@ -46,9 +60,11 @@ import io.reactivex.rxjava3.schedulers.Schedulers; /** * Manages all the stuff around determining if a user is registered or not. */ -class DirectoryHelper { +class ContactDiscoveryRefreshV1 { - private static final String TAG = Log.tag(DirectoryHelper.class); + private static final String TAG = Log.tag(ContactDiscoveryRefreshV1.class); + + private static final int MAX_NUMBERS = 20_500; @WorkerThread static @NonNull RefreshResult refreshAll(@NonNull Context context) throws IOException { @@ -95,11 +111,11 @@ class DirectoryHelper { Stopwatch stopwatch = new Stopwatch("refresh"); - DirectoryResult result; + ContactIntersection result; if (FeatureFlags.cdsh()) { - result = ContactDiscoveryHsmV1.getDirectoryResult(databaseNumbers, systemNumbers); + result = getIntersectionWithHsm(databaseNumbers, systemNumbers); } else { - result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers); + result = getIntersection(context, databaseNumbers, systemNumbers); } stopwatch.split("network"); @@ -165,7 +181,7 @@ class DirectoryHelper { .map(Recipient::resolved) .filter(Recipient::isRegistered) .filter(Recipient::hasServiceId) - .filter(DirectoryHelper::hasCommunicatedWith) + .filter(ContactDiscoveryRefreshV1::hasCommunicatedWith) .toList(); ProfileService profileService = new ProfileService(ApplicationDependencies.getGroupsV2Operations().getProfileOperations(), @@ -204,14 +220,100 @@ class DirectoryHelper { return SignalDatabase.threads().hasThread(recipient.getId()) || (recipient.hasServiceId() && SignalDatabase.sessions().hasSessionFor(localAci, recipient.requireServiceId().toString())); } - static class DirectoryResult { + /** + * Retrieves the contact intersection using the current production CDS. + */ + private static ContactIntersection getIntersection(@NonNull Context context, + @NonNull Set databaseNumbers, + @NonNull Set systemNumbers) + throws IOException + { + Set allNumbers = SetUtil.union(databaseNumbers, systemNumbers); + FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(allNumbers, databaseNumbers); + Set sanitizedNumbers = sanitizeNumbers(inputResult.getNumbers()); + Set ignoredNumbers = new HashSet<>(); + + if (sanitizedNumbers.size() > MAX_NUMBERS) { + Set randomlySelected = randomlySelect(sanitizedNumbers, MAX_NUMBERS); + + ignoredNumbers = SetUtil.difference(sanitizedNumbers, randomlySelected); + sanitizedNumbers = randomlySelected; + } + + SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); + KeyStore iasKeyStore = getIasKeyStore(context); + + try { + Map results = accountManager.getRegisteredUsers(iasKeyStore, sanitizedNumbers, BuildConfig.CDS_MRENCLAVE); + FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(results, inputResult); + + return new ContactIntersection(outputResult.getNumbers(), outputResult.getRewrites(), ignoredNumbers); + } catch (SignatureException | UnauthenticatedQuoteException | UnauthenticatedResponseException | Quote.InvalidQuoteFormatException | InvalidKeyException e) { + Log.w(TAG, "Attestation error.", e); + throw new IOException(e); + } + } + + /** + * Retrieves the contact intersection using an HSM-backed implementation of CDS that is being tested. + */ + private static ContactIntersection getIntersectionWithHsm(@NonNull Set databaseNumbers, + @NonNull Set systemNumbers) + throws IOException + { + Set allNumbers = SetUtil.union(databaseNumbers, systemNumbers); + FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(allNumbers, databaseNumbers); + Set sanitizedNumbers = sanitizeNumbers(inputResult.getNumbers()); + Set ignoredNumbers = new HashSet<>(); + + if (sanitizedNumbers.size() > MAX_NUMBERS) { + Set randomlySelected = randomlySelect(sanitizedNumbers, MAX_NUMBERS); + + ignoredNumbers = SetUtil.difference(sanitizedNumbers, randomlySelected); + sanitizedNumbers = randomlySelected; + } + + SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); + + try { + Map results = accountManager.getRegisteredUsersWithCdshV1(sanitizedNumbers, BuildConfig.CDSH_PUBLIC_KEY, BuildConfig.CDSH_CODE_HASH); + FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(results, inputResult); + + return new ContactIntersection(outputResult.getNumbers(), outputResult.getRewrites(), ignoredNumbers); + } catch (IOException e) { + Log.w(TAG, "Attestation error.", e); + throw new IOException(e); + } + } + + private static @NonNull Set randomlySelect(@NonNull Set numbers, int max) { + List list = new ArrayList<>(numbers); + Collections.shuffle(list); + + return new HashSet<>(list.subList(0, max)); + } + + private static KeyStore getIasKeyStore(@NonNull Context context) { + try { + TrustStore contactTrustStore = new IasTrustStore(context); + + KeyStore keyStore = KeyStore.getInstance("BKS"); + keyStore.load(contactTrustStore.getKeyStoreInputStream(), contactTrustStore.getKeyStorePassword().toCharArray()); + + return keyStore; + } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + + static class ContactIntersection { private final Map registeredNumbers; private final Map numberRewrites; private final Set ignoredNumbers; - DirectoryResult(@NonNull Map registeredNumbers, - @NonNull Map numberRewrites, - @NonNull Set ignoredNumbers) + ContactIntersection(@NonNull Map registeredNumbers, + @NonNull Map numberRewrites, + @NonNull Set ignoredNumbers) { this.registeredNumbers = registeredNumbers; this.numberRewrites = numberRewrites; diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryV2.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryV2.java deleted file mode 100644 index 59ce3d457e..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryV2.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.thoughtcrime.securesms.contacts.sync; - -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.WorkerThread; - -import com.annimon.stream.Collectors; -import com.annimon.stream.Stream; - -import org.signal.core.util.logging.Log; -import org.signal.libsignal.protocol.InvalidKeyException; -import org.thoughtcrime.securesms.BuildConfig; -import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper.DirectoryResult; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.push.IasTrustStore; -import org.signal.core.util.SetUtil; -import org.whispersystems.signalservice.api.SignalServiceAccountManager; -import org.whispersystems.signalservice.api.push.ACI; -import org.whispersystems.signalservice.api.push.TrustStore; -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.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Uses CDS to map E164's to UUIDs. - */ -class ContactDiscoveryV2 { - - private static final String TAG = Log.tag(ContactDiscoveryV2.class); - - private static final int MAX_NUMBERS = 20_500; - - @WorkerThread - static DirectoryResult getDirectoryResult(@NonNull Context context, - @NonNull Set databaseNumbers, - @NonNull Set systemNumbers) - throws IOException - { - Set allNumbers = SetUtil.union(databaseNumbers, systemNumbers); - FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(allNumbers, databaseNumbers); - Set sanitizedNumbers = sanitizeNumbers(inputResult.getNumbers()); - Set ignoredNumbers = new HashSet<>(); - - if (sanitizedNumbers.size() > MAX_NUMBERS) { - Set randomlySelected = randomlySelect(sanitizedNumbers, MAX_NUMBERS); - - ignoredNumbers = SetUtil.difference(sanitizedNumbers, randomlySelected); - sanitizedNumbers = randomlySelected; - } - - SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); - KeyStore iasKeyStore = getIasKeyStore(context); - - try { - Map results = accountManager.getRegisteredUsers(iasKeyStore, sanitizedNumbers, BuildConfig.CDS_MRENCLAVE); - FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(results, inputResult); - - return new DirectoryResult(outputResult.getNumbers(), outputResult.getRewrites(), ignoredNumbers); - } catch (SignatureException | UnauthenticatedQuoteException | UnauthenticatedResponseException | Quote.InvalidQuoteFormatException |InvalidKeyException e) { - Log.w(TAG, "Attestation error.", e); - throw new IOException(e); - } - } - - static @NonNull DirectoryResult getDirectoryResult(@NonNull Context context, @NonNull String number) throws IOException { - return getDirectoryResult(context, Collections.singleton(number), Collections.singleton(number)); - } - - private static Set sanitizeNumbers(@NonNull Set numbers) { - return Stream.of(numbers).filter(number -> { - try { - return number.startsWith("+") && number.length() > 1 && number.charAt(1) != '0' && Long.parseLong(number.substring(1)) > 0; - } catch (NumberFormatException e) { - return false; - } - }).collect(Collectors.toSet()); - } - - private static @NonNull Set randomlySelect(@NonNull Set numbers, int max) { - List list = new ArrayList<>(numbers); - Collections.shuffle(list); - - return new HashSet<>(list.subList(0, max)); - } - - private static KeyStore getIasKeyStore(@NonNull Context context) { - try { - TrustStore contactTrustStore = new IasTrustStore(context); - - KeyStore keyStore = KeyStore.getInstance("BKS"); - keyStore.load(contactTrustStore.getKeyStoreInputStream(), contactTrustStore.getKeyStorePassword().toCharArray()); - - return keyStore; - } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) { - throw new AssertionError(e); - } - } -}