diff --git a/app/build.gradle b/app/build.gradle index 18e6eac71d..ec6a437e3f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -149,6 +149,7 @@ android { buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\"" buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\"" buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\"" + buildConfigField "String", "SIGNAL_CDSH_URL", "\"https://cdsh.staging.signal.org\"" buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\"" buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\"" buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\"" @@ -157,6 +158,8 @@ android { buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\"" buildConfigField "int", "CONTENT_PROXY_PORT", "443" buildConfigField "String", "SIGNAL_AGENT", "\"OWA\"" + buildConfigField "String", "CDSH_PUBLIC_KEY", "\"2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74\"" + buildConfigField "String", "CDSH_CODE_HASH", "\"ec31a51880d19a5e9e0fed404740c1a3ff53a553125564b745acce475f0fded8\"" buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\"" buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," + "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " + diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryV3.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryV3.java new file mode 100644 index 0000000000..4616120216 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryV3.java @@ -0,0 +1,75 @@ +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.thoughtcrime.securesms.util.SetUtil; +import org.whispersystems.signalservice.api.SignalServiceAccountManager; + +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.UUID; +import java.util.stream.Collectors; + +/** + * Uses CDS to map E164's to UUIDs. + */ +class ContactDiscoveryV3 { + + private static final String TAG = Log.tag(ContactDiscoveryV3.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.getRegisteredUsersWithCdsh(sanitizedNumbers, BuildConfig.CDSH_PUBLIC_KEY, BuildConfig.CDSH_CODE_HASH); + FuzzyPhoneNumberHelper.OutputResultV2 outputResult = FuzzyPhoneNumberHelper.generateOutputV2(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/DirectoryHelper.java index a57ceb6b3f..0901fc00ef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java @@ -43,6 +43,7 @@ import org.thoughtcrime.securesms.registration.RegistrationUtil; import org.thoughtcrime.securesms.sms.IncomingJoinedMessage; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.CursorUtil; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.ProfileUtil; import org.thoughtcrime.securesms.util.SetUtil; import org.thoughtcrime.securesms.util.Stopwatch; @@ -230,7 +231,12 @@ public class DirectoryHelper { Stopwatch stopwatch = new Stopwatch("refresh"); - DirectoryResult result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers); + DirectoryResult result; + if (FeatureFlags.cdsh()) { + result = ContactDiscoveryV3.getDirectoryResult(databaseNumbers, systemNumbers); + } else { + result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers); + } stopwatch.split("network"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java index 19c73940b0..10b10a3eb8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java +++ b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java @@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; +import org.whispersystems.signalservice.internal.configuration.SignalCdshUrl; import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; @@ -159,7 +160,6 @@ public class SignalServiceNetworkAccess { final SignalContactDiscoveryUrl omanGoogleDiscovery = new SignalContactDiscoveryUrl("https://www.google.com.om/directory", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); final SignalContactDiscoveryUrl qatarGoogleDiscovery = new SignalContactDiscoveryUrl("https://www.google.com.qa/directory", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalKeyBackupServiceUrl baseGoogleKbs = new SignalKeyBackupServiceUrl("https://www.google.com/backup", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); final SignalKeyBackupServiceUrl baseAndroidKbs = new SignalKeyBackupServiceUrl("https://android.clients.google.com/backup", SERVICE_REFLECTOR_HOST, trustStore, PLAY_CONNECTION_SPEC); final SignalKeyBackupServiceUrl mapsOneAndroidKbs = new SignalKeyBackupServiceUrl("https://clients3.google.com/backup", SERVICE_REFLECTOR_HOST, trustStore, GMAPS_CONNECTION_SPEC); @@ -204,6 +204,7 @@ public class SignalServiceNetworkAccess { new SignalContactDiscoveryUrl[] {egyptGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery}, new SignalKeyBackupServiceUrl[] {egyptGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs}, new SignalStorageUrl[] {egyptGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage}, + new SignalCdshUrl[] {new SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, new SignalServiceTrustStore(context))}, interceptors, dns, Optional.absent(), @@ -215,6 +216,7 @@ public class SignalServiceNetworkAccess { new SignalContactDiscoveryUrl[] {uaeGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery}, new SignalKeyBackupServiceUrl[] {uaeGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs}, new SignalStorageUrl[] {uaeGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage}, + new SignalCdshUrl[] {new SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, new SignalServiceTrustStore(context))}, interceptors, dns, Optional.absent(), @@ -226,6 +228,7 @@ public class SignalServiceNetworkAccess { new SignalContactDiscoveryUrl[] {omanGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery}, new SignalKeyBackupServiceUrl[] {omanGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs}, new SignalStorageUrl[] {omanGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage}, + new SignalCdshUrl[] {new SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, new SignalServiceTrustStore(context))}, interceptors, dns, Optional.absent(), @@ -238,6 +241,7 @@ public class SignalServiceNetworkAccess { new SignalContactDiscoveryUrl[] {qatarGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery}, new SignalKeyBackupServiceUrl[] {qatarGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs}, new SignalStorageUrl[] {qatarGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage}, + new SignalCdshUrl[] {new SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, new SignalServiceTrustStore(context))}, interceptors, dns, Optional.absent(), @@ -249,6 +253,7 @@ public class SignalServiceNetworkAccess { Stream.of(fastUrls).map(url -> new SignalContactDiscoveryUrl(url, DIRECTORY_FASTLY_HOST, new DomainFrontingDigicertTrustStore(context), APP_CONNECTION_SPEC)).toArray(SignalContactDiscoveryUrl[]::new), Stream.of(fastUrls).map(url -> new SignalKeyBackupServiceUrl(url, KBS_FASTLY_HOST, new DomainFrontingDigicertTrustStore(context), APP_CONNECTION_SPEC)).toArray(SignalKeyBackupServiceUrl[]::new), Stream.of(fastUrls).map(url -> new SignalStorageUrl(url, STORAGE_FASTLY_HOST, new DomainFrontingDigicertTrustStore(context), APP_CONNECTION_SPEC)).toArray(SignalStorageUrl[]::new), + new SignalCdshUrl[] {new SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, new SignalServiceTrustStore(context))}, interceptors, dns, Optional.absent(), @@ -261,6 +266,7 @@ public class SignalServiceNetworkAccess { new SignalContactDiscoveryUrl[] {new SignalContactDiscoveryUrl(BuildConfig.SIGNAL_CONTACT_DISCOVERY_URL, new SignalServiceTrustStore(context))}, new SignalKeyBackupServiceUrl[] { new SignalKeyBackupServiceUrl(BuildConfig.SIGNAL_KEY_BACKUP_URL, new SignalServiceTrustStore(context)) }, new SignalStorageUrl[] {new SignalStorageUrl(BuildConfig.STORAGE_URL, new SignalServiceTrustStore(context))}, + new SignalCdshUrl[] {new SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, new SignalServiceTrustStore(context))}, interceptors, dns, SignalStore.proxy().isProxyEnabled() ? Optional.of(SignalStore.proxy().getProxy()) : Optional.absent(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 6ccecf0042..502c0ab482 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -85,6 +85,7 @@ public final class FeatureFlags { private static final String GROUP_CALL_RINGING = "android.calling.groupCallRinging"; private static final String CHANGE_NUMBER_ENABLED = "android.changeNumber"; private static final String DONOR_BADGES = "android.donorBadges"; + private static final String CDSH = "android.cdsh"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -121,7 +122,8 @@ public final class FeatureFlags { RETRY_RECEIPTS, SUGGEST_SMS_BLACKLIST, MAX_GROUP_CALL_RING_SIZE, - GROUP_CALL_RINGING + GROUP_CALL_RINGING, + CDSH ); @VisibleForTesting @@ -174,7 +176,8 @@ public final class FeatureFlags { RETRY_RECEIPTS, SENDER_KEY, MAX_GROUP_CALL_RING_SIZE, - GROUP_CALL_RINGING + GROUP_CALL_RINGING, + CDSH ); /** @@ -410,6 +413,10 @@ public final class FeatureFlags { } } + public static boolean cdsh() { + return Environment.IS_STAGING && getBoolean(CDSH, false); + } + /** Only for rendering debug info. */ public static synchronized @NonNull Map getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); diff --git a/app/witness-verifications.gradle b/app/witness-verifications.gradle index aab7d61898..cf33df6296 100644 --- a/app/witness-verifications.gradle +++ b/app/witness-verifications.gradle @@ -591,11 +591,11 @@ dependencyVerification { ['org.threeten:threetenbp:1.3.6', 'f4c23ffaaed717c3b99c003e0ee02d6d66377fd47d866fec7d971bd8644fc1a7'], - ['org.whispersystems:signal-client-android:0.9.4', - '5b4d8e0b37701caefe6089bdf09667716fea5829f105373e4bfce3041e7c6387'], + ['org.whispersystems:signal-client-android:0.9.5', + 'd63788841fe2c8d15a144c99abf188c1c3478d31701b5cdb4c19761676b9049d'], - ['org.whispersystems:signal-client-java:0.9.4', - '629fb84dbbf5663cbfda0cb87420b0f64ad9902088c575478b04009cce9cbf8a'], + ['org.whispersystems:signal-client-java:0.9.5', + 'f00784bf49e75744e1d6c128136a8849b42551b2075cc392f7fe237793a13f7d'], ['pl.tajchert:waitingdots:0.1.0', '2835d49e0787dbcb606c5a60021ced66578503b1e9fddcd7a5ef0cd5f095ba2c'], diff --git a/dependencies.gradle b/dependencies.gradle index 21fd356203..a76907b82b 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,7 +1,7 @@ dependencyResolutionManagement { versionCatalogs { libs { - version('signal-client', '0.9.4') + version('signal-client', '0.9.5') version('zkgroup', '0.7.0') version('exoplayer', '2.15.0') version('androidx-camera', '1.0.0-beta11') diff --git a/device-transfer/lib/witness-verifications.gradle b/device-transfer/lib/witness-verifications.gradle index 9dca5a7b2a..045b14b60a 100644 --- a/device-transfer/lib/witness-verifications.gradle +++ b/device-transfer/lib/witness-verifications.gradle @@ -81,7 +81,7 @@ dependencyVerification { ['org.greenrobot:eventbus:3.0.0', '180d4212467df06f2fbc9c8d8a2984533ac79c87769ad883bc421612f0b4e17c'], - ['org.whispersystems:signal-client-java:0.9.4', - '629fb84dbbf5663cbfda0cb87420b0f64ad9902088c575478b04009cce9cbf8a'], + ['org.whispersystems:signal-client-java:0.9.5', + 'f00784bf49e75744e1d6c128136a8849b42551b2075cc392f7fe237793a13f7d'], ] } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 955e882506..0e4cee7792 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -38,6 +38,7 @@ import org.whispersystems.signalservice.api.push.exceptions.NoContentException; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; +import org.whispersystems.signalservice.api.services.CdshService; import org.whispersystems.signalservice.api.storage.SignalStorageCipher; import org.whispersystems.signalservice.api.storage.SignalStorageManifest; import org.whispersystems.signalservice.api.storage.SignalStorageModels; @@ -97,6 +98,8 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import io.reactivex.rxjava3.core.Single; + import static org.whispersystems.signalservice.internal.push.ProvisioningProtos.ProvisionMessage; import static org.whispersystems.signalservice.internal.push.ProvisioningProtos.ProvisioningVersion; @@ -110,10 +113,11 @@ public class SignalServiceAccountManager { private static final String TAG = SignalServiceAccountManager.class.getSimpleName(); - private final PushServiceSocket pushServiceSocket; - private final CredentialsProvider credentials; - private final String userAgent; - private final GroupsV2Operations groupsV2Operations; + private final PushServiceSocket pushServiceSocket; + private final CredentialsProvider credentials; + private final String userAgent; + private final GroupsV2Operations groupsV2Operations; + private final SignalServiceConfiguration configuration; /** * Construct a SignalServiceAccountManager. @@ -145,6 +149,7 @@ public class SignalServiceAccountManager { this.pushServiceSocket = new PushServiceSocket(configuration, credentialsProvider, signalAgent, groupsV2Operations.getProfileOperations(), automaticNetworkRetry); this.credentials = credentialsProvider; this.userAgent = signalAgent; + this.configuration = configuration; } public byte[] getSenderCertificate() throws IOException { @@ -437,6 +442,7 @@ public class SignalServiceAccountManager { return activeTokens; } + @SuppressWarnings("SameParameterValue") public Map getRegisteredUsers(KeyStore iasKeyStore, Set e164numbers, String mrenclave) throws IOException, Quote.InvalidQuoteFormatException, UnauthenticatedQuoteException, SignatureException, UnauthenticatedResponseException, InvalidKeyException { @@ -481,6 +487,31 @@ public class SignalServiceAccountManager { } } + public Map getRegisteredUsersWithCdsh(Set e164numbers, String hexPublicKey, String hexCodeHash) + throws IOException + { + CdshService service = new CdshService(configuration, hexPublicKey, hexCodeHash); + Single>> result = service.getRegisteredUsers(e164numbers); + + ServiceResponse> response; + try { + response = result.blockingGet(); + } catch (Exception e) { + throw new RuntimeException("Unexpected exception when retrieving registered users!", e); + } + + if (response.getResult().isPresent()) { + return response.getResult().get(); + } else if (response.getApplicationError().isPresent()) { + throw new IOException(response.getApplicationError().get()); + } else if (response.getExecutionError().isPresent()) { + throw new IOException(response.getExecutionError().get()); + } else { + throw new IOException("Missing result!"); + } + } + + public Optional getStorageManifest(StorageKey storageKey) throws IOException { try { String authToken = this.pushServiceSocket.getStorageAuth(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/CdshService.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/CdshService.java new file mode 100644 index 0000000000..dcf16c7b46 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/CdshService.java @@ -0,0 +1,184 @@ +package org.whispersystems.signalservice.api.services; + +import org.signal.libsignal.hsmenclave.HsmEnclaveClient; +import org.whispersystems.libsignal.logging.Log; +import org.whispersystems.libsignal.util.ByteUtil; +import org.whispersystems.libsignal.util.Pair; +import org.whispersystems.signalservice.api.push.TrustStore; +import org.whispersystems.signalservice.api.util.Tls12SocketFactory; +import org.whispersystems.signalservice.internal.ServiceResponse; +import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; +import org.whispersystems.signalservice.internal.util.BlacklistingTrustManager; +import org.whispersystems.signalservice.internal.util.Hex; +import org.whispersystems.signalservice.internal.util.Util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.core.SingleEmitter; +import io.reactivex.rxjava3.core.SingleOnSubscribe; +import io.reactivex.rxjava3.subjects.PublishSubject; +import okhttp3.ConnectionSpec; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import okio.ByteString; + +/** + * Handles network interactions with CDSH, the HSM-backed CDS service. + */ +public final class CdshService { + + private static final String TAG = CdshService.class.getSimpleName(); + + private static final int VERSION = 1; + + private final OkHttpClient client; + private final HsmEnclaveClient enclave; + private final String baseUrl; + private final String hexPublicKey; + private final String hexCodeHash; + + public CdshService(SignalServiceConfiguration configuration, String hexPublicKey, String hexCodeHash) { + this.baseUrl = configuration.getSignalCdshUrls()[0].getUrl(); + this.hexPublicKey = hexPublicKey; + this.hexCodeHash = hexCodeHash; + + Pair socketFactory = createTlsSocketFactory(configuration.getSignalCdshUrls()[0].getTrustStore()); + + this.client = new OkHttpClient.Builder().sslSocketFactory(new Tls12SocketFactory(socketFactory.first()), + socketFactory.second()) + .connectionSpecs(Util.immutableList(ConnectionSpec.RESTRICTED_TLS)) + .readTimeout(30, TimeUnit.SECONDS) + .connectTimeout(30, TimeUnit.SECONDS) + .build(); + + + try { + this.enclave = new HsmEnclaveClient(Hex.fromStringCondensed(hexPublicKey), + Collections.singletonList(Hex.fromStringCondensed(hexCodeHash))); + } catch (IOException e) { + throw new IllegalArgumentException("Badly-formatted public key or code hash!", e); + } + } + + public Single>> getRegisteredUsers(Set e164Numbers) { + return Single.create(emitter -> { + AtomicReference stage = new AtomicReference<>(Stage.WAITING_TO_INITIALIZE); + List addressBook = e164Numbers.stream().map(e -> e.substring(1)).collect(Collectors.toList()); + + String url = String.format("%s/discovery/%s/%s", baseUrl, hexPublicKey, hexCodeHash); + Request request = new Request.Builder().url(url).build(); + WebSocket webSocket = client.newWebSocket(request, new WebSocketListener() { + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + switch (stage.get()) { + case WAITING_TO_INITIALIZE: + enclave.completeHandshake(bytes.toByteArray()); + + byte[] request = enclave.establishedSend(buildPlaintextRequest(addressBook)); + + stage.set(Stage.WAITING_FOR_RESPONSE); + webSocket.send(ByteString.of(request)); + + break; + case WAITING_FOR_RESPONSE: + byte[] response = enclave.establishedRecv(bytes.toByteArray()); + + try { + Map out = parseResponse(addressBook, response); + emitter.onSuccess(ServiceResponse.forResult(out, 200, null)); + } catch (IOException e) { + emitter.onSuccess(ServiceResponse.forUnknownError(e)); + } finally { + webSocket.close(1000, "OK"); + } + + break; + case FAILURE: + Log.w(TAG, "Received a message after we entered the failure state! Ignoring."); + webSocket.close(1000, "OK"); + break; + } + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + emitter.onSuccess(ServiceResponse.forApplicationError(t, response != null ? response.code() : 0, null)); + stage.set(Stage.FAILURE); + webSocket.close(1000, "OK"); + } + }); + + webSocket.send(ByteString.of(enclave.initialRequest())); + }); + } + + private static byte[] buildPlaintextRequest(List addressBook) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + outputStream.write(VERSION); + + for (String e164 : addressBook) { + outputStream.write(ByteUtil.longToByteArray(Long.parseLong(e164))); + } + + return outputStream.toByteArray(); + } catch (IOException e) { + throw new AssertionError("Failed to write bytes to the output stream?"); + } + } + + private static Map parseResponse(List addressBook, byte[] plaintextResponse) throws IOException { + Map results = new HashMap<>(); + + try (DataInputStream uuidInputStream = new DataInputStream(new ByteArrayInputStream(plaintextResponse))) { + for (String candidate : addressBook) { + long candidateUuidHigh = uuidInputStream.readLong(); + long candidateUuidLow = uuidInputStream.readLong(); + if (candidateUuidHigh != 0 || candidateUuidLow != 0) { + results.put('+' + candidate, new UUID(candidateUuidHigh, candidateUuidLow)); + } + } + } + + return results; + } + + private static Pair createTlsSocketFactory(TrustStore trustStore) { + try { + SSLContext context = SSLContext.getInstance("TLS"); + TrustManager[] trustManagers = BlacklistingTrustManager.createFor(trustStore); + context.init(null, trustManagers, null); + + return new Pair<>(context.getSocketFactory(), (X509TrustManager) trustManagers[0]); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new AssertionError(e); + } + } + + private enum Stage { + WAITING_TO_INITIALIZE, WAITING_FOR_RESPONSE, FAILURE + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalCdshUrl.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalCdshUrl.java new file mode 100644 index 0000000000..de2f5c7cf3 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalCdshUrl.java @@ -0,0 +1,17 @@ +package org.whispersystems.signalservice.internal.configuration; + + +import org.whispersystems.signalservice.api.push.TrustStore; + +import okhttp3.ConnectionSpec; + +public class SignalCdshUrl extends SignalUrl { + + public SignalCdshUrl(String url, TrustStore trustStore) { + super(url, trustStore); + } + + public SignalCdshUrl(String url, String hostHeader, TrustStore trustStore, ConnectionSpec connectionSpec) { + super(url, hostHeader, trustStore, connectionSpec); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java index 85f91fbfc5..91ce2a7565 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java @@ -13,6 +13,7 @@ public final class SignalServiceConfiguration { private final SignalServiceUrl[] signalServiceUrls; private final Map signalCdnUrlMap; private final SignalContactDiscoveryUrl[] signalContactDiscoveryUrls; + private final SignalCdshUrl[] signalCdshUrls; private final SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls; private final SignalStorageUrl[] signalStorageUrls; private final List networkInterceptors; @@ -25,6 +26,7 @@ public final class SignalServiceConfiguration { SignalContactDiscoveryUrl[] signalContactDiscoveryUrls, SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls, SignalStorageUrl[] signalStorageUrls, + SignalCdshUrl[] signalCdshUrls, List networkInterceptors, Optional dns, Optional proxy, @@ -33,6 +35,7 @@ public final class SignalServiceConfiguration { this.signalServiceUrls = signalServiceUrls; this.signalCdnUrlMap = signalCdnUrlMap; this.signalContactDiscoveryUrls = signalContactDiscoveryUrls; + this.signalCdshUrls = signalCdshUrls; this.signalKeyBackupServiceUrls = signalKeyBackupServiceUrls; this.signalStorageUrls = signalStorageUrls; this.networkInterceptors = networkInterceptors; @@ -53,6 +56,10 @@ public final class SignalServiceConfiguration { return signalContactDiscoveryUrls; } + public SignalCdshUrl[] getSignalCdshUrls() { + return signalCdshUrls; + } + public SignalKeyBackupServiceUrl[] getSignalKeyBackupServiceUrls() { return signalKeyBackupServiceUrls; } diff --git a/libsignal/service/witness-verifications.gradle b/libsignal/service/witness-verifications.gradle index 2e0002f252..55f704a3ff 100644 --- a/libsignal/service/witness-verifications.gradle +++ b/libsignal/service/witness-verifications.gradle @@ -36,7 +36,7 @@ dependencyVerification { ['org.threeten:threetenbp:1.3.6', 'f4c23ffaaed717c3b99c003e0ee02d6d66377fd47d866fec7d971bd8644fc1a7'], - ['org.whispersystems:signal-client-java:0.9.4', - '629fb84dbbf5663cbfda0cb87420b0f64ad9902088c575478b04009cce9cbf8a'], + ['org.whispersystems:signal-client-java:0.9.5', + 'f00784bf49e75744e1d6c128136a8849b42551b2075cc392f7fe237793a13f7d'], ] }