From 56eae8c7bff7af255f9a0bf30ef5847497510af5 Mon Sep 17 00:00:00 2001 From: Alex Konradi Date: Fri, 1 Mar 2024 14:45:37 -0500 Subject: [PATCH] Add libsignal-net CDSI implementation. --- app/build.gradle.kts | 2 + .../sync/ContactDiscoveryRefreshV2.kt | 3 +- .../securesms/util/FeatureFlags.java | 12 +++- .../api/SignalServiceAccountManager.java | 5 +- .../api/services/CdsiV2Service.java | 56 ++++++++++++++++--- 5 files changed, 66 insertions(+), 12 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 72790bf668..6538ba5561 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -208,6 +208,7 @@ android { buildConfigField("String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\"") buildConfigField("String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/registration/generate.html\"") buildConfigField("String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\"") + buildConfigField("org.signal.libsignal.net.Network.Environment", "LIBSIGNAL_NET_ENV", "org.signal.libsignal.net.Network.Environment.PRODUCTION") buildConfigField("String", "BUILD_DISTRIBUTION_TYPE", "\"unset\"") buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"unset\"") @@ -385,6 +386,7 @@ android { buildConfigField("String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\"") buildConfigField("String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/staging/registration/generate.html\"") buildConfigField("String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\"") + buildConfigField("org.signal.libsignal.net.Network.Environment", "LIBSIGNAL_NET_ENV", "org.signal.libsignal.net.Network.Environment.STAGING") buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\"") buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\"") diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt index b0df28ff49..7f6661340a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt @@ -126,7 +126,8 @@ object ContactDiscoveryRefreshV2 { SignalDatabase.recipients.getAllServiceIdProfileKeyPairs(), Optional.ofNullable(token), BuildConfig.CDSI_MRENCLAVE, - timeoutMs + timeoutMs, + if (FeatureFlags.useLibsignalNetForCdsiLookup()) BuildConfig.LIBSIGNAL_NET_ENV else null ) { tokenToSave -> stopwatch.split("network-pre-token") if (!isPartialRefresh) { 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 2a27c18683..fbe2f5148a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -123,6 +123,7 @@ public final class FeatureFlags { private static final String RETRY_RECEIPT_MAX_COUNT = "android.retryReceipt.maxCount"; private static final String RETRY_RECEIPT_MAX_COUNT_RESET_AGE = "android.retryReceipt.maxCountResetAge"; private static final String PREKEY_FORCE_REFRESH_INTERVAL = "android.prekeyForceRefreshInterval"; + private static final String CDSI_LIBSIGNAL_NET = "android.cds.libsignal"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -198,7 +199,8 @@ public final class FeatureFlags { VIDEO_RECORD_1X_ZOOM, RETRY_RECEIPT_MAX_COUNT, RETRY_RECEIPT_MAX_COUNT_RESET_AGE, - PREKEY_FORCE_REFRESH_INTERVAL + PREKEY_FORCE_REFRESH_INTERVAL, + CDSI_LIBSIGNAL_NET ); @VisibleForTesting @@ -271,7 +273,8 @@ public final class FeatureFlags { VIDEO_RECORD_1X_ZOOM, RETRY_RECEIPT_MAX_COUNT, RETRY_RECEIPT_MAX_COUNT_RESET_AGE, - PREKEY_FORCE_REFRESH_INTERVAL + PREKEY_FORCE_REFRESH_INTERVAL, + CDSI_LIBSIGNAL_NET ); /** @@ -706,6 +709,11 @@ public final class FeatureFlags { return getLong(PREKEY_FORCE_REFRESH_INTERVAL, TimeUnit.HOURS.toMillis(1)); } + /** Make CDSI lookups via libsignal-net instead of native websocket. */ + public static boolean useLibsignalNetForCdsiLookup() { + return getBoolean(CDSI_LIBSIGNAL_NET, false); + } + /** Only for rendering debug info. */ public static synchronized @NonNull Map getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); 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 8343fd234f..676a7dd356 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 @@ -8,6 +8,7 @@ package org.whispersystems.signalservice.api; import com.squareup.wire.FieldEncoding; +import org.signal.libsignal.net.Network; import org.signal.libsignal.protocol.IdentityKeyPair; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.ecc.ECPublicKey; @@ -130,7 +131,6 @@ public class SignalServiceAccountManager { private final GroupsV2Operations groupsV2Operations; private final SignalServiceConfiguration configuration; - /** * Construct a SignalServiceAccountManager. * @param configuration The URL for the Signal Service. @@ -365,11 +365,12 @@ public class SignalServiceAccountManager { Optional token, String mrEnclave, Long timeoutMs, + @Nullable Network.Environment libsignalNetEnv, Consumer tokenSaver) throws IOException { CdsiAuthResponse auth = pushServiceSocket.getCdsiAuth(); - CdsiV2Service service = new CdsiV2Service(configuration, mrEnclave); + CdsiV2Service service = new CdsiV2Service(configuration, mrEnclave, libsignalNetEnv); CdsiV2Service.Request request = new CdsiV2Service.Request(previousE164s, newE164s, serviceIds, token); Single> single = service.getRegisteredUsers(auth.getUsername(), auth.getPassword(), request, tokenSaver); diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/CdsiV2Service.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/CdsiV2Service.java index fafe613183..1ca90e5acd 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/CdsiV2Service.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/CdsiV2Service.java @@ -2,6 +2,10 @@ package org.whispersystems.signalservice.api.services; import org.signal.cdsi.proto.ClientRequest; import org.signal.cdsi.proto.ClientResponse; +import org.signal.core.util.logging.Log; +import org.signal.libsignal.net.CdsiLookupRequest; +import org.signal.libsignal.net.CdsiLookupResponse; +import org.signal.libsignal.net.Network; import org.signal.libsignal.protocol.util.ByteUtil; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; @@ -16,6 +20,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; +import java.time.Duration; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -24,9 +29,11 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.concurrent.Future; import java.util.function.Consumer; import java.util.stream.Collectors; +import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Single; import okio.ByteString; @@ -40,16 +47,35 @@ public final class CdsiV2Service { private static final UUID EMPTY_UUID = new UUID(0, 0); private static final int RESPONSE_ITEM_SIZE = 8 + 16 + 16; // 1 uint64 + 2 UUIDs - private final CdsiSocket cdsiSocket; + private static final Duration LIBSIGNAL_CDSI_TIMEOUT = Duration.ofSeconds(10); - public CdsiV2Service(SignalServiceConfiguration configuration, String mrEnclave) { - this.cdsiSocket = new CdsiSocket(configuration, mrEnclave); - } + private final CdsiRequestHandler cdsiRequestHandler; + + public CdsiV2Service(SignalServiceConfiguration configuration, String mrEnclave, Network.Environment libsignalEnv) { + + if (libsignalEnv != null) { + Network network = new Network(libsignalEnv); + this.cdsiRequestHandler = (username, password, request, tokenSaver) -> { + try { + Log.i(TAG, "Starting CDSI lookup via libsignal-net"); + Future cdsiRequest = network.cdsiLookup(username, password, buildLibsignalRequest(request), LIBSIGNAL_CDSI_TIMEOUT, tokenSaver); + return Single.fromFuture(cdsiRequest).map(CdsiV2Service::parseLibsignalResponse).toObservable(); + } catch (Exception exception) { + return Observable.error(exception); + } + }; + } else { + CdsiSocket cdsiSocket = new CdsiSocket(configuration, mrEnclave); + this.cdsiRequestHandler = (username, password, request, tokenSaver) -> { + return cdsiSocket + .connect(username, password, buildClientRequest(request), tokenSaver) + .map(CdsiV2Service::parseEntries); + }; + } + } public Single> getRegisteredUsers(String username, String password, Request request, Consumer tokenSaver) { - return cdsiSocket - .connect(username, password, buildClientRequest(request), tokenSaver) - .map(CdsiV2Service::parseEntries) + return cdsiRequestHandler.handleRequest(username, password, request, tokenSaver) .collect(Collectors.toList()) .flatMap(pages -> { Map all = new HashMap<>(); @@ -139,6 +165,18 @@ public final class CdsiV2Service { return ByteString.of(os.toByteArray()); } + private static CdsiLookupRequest buildLibsignalRequest(Request request) { + HashMap serviceIds = new HashMap<>(request.serviceIds.size()); + request.serviceIds.forEach((key, value) -> serviceIds.put(key.getLibSignalServiceId(), value)); + return new CdsiLookupRequest(request.previousE164s, request.newE164s, serviceIds, false, Optional.ofNullable(request.token)); + } + + private static Response parseLibsignalResponse(CdsiLookupResponse response) { + HashMap responses = new HashMap<>(response.entries().size()); + response.entries().forEach((key, value) -> responses.put(key, new ResponseItem(new PNI(value.pni), Optional.ofNullable(value.aci).map(ACI::new)))); + return new Response(responses, response.debugPermitsUsed); + } + private static List parseAndSortE164Strings(Collection e164s) { return e164s.stream() .map(Long::parseLong) @@ -216,4 +254,8 @@ public final class CdsiV2Service { return aci.isPresent(); } } + + private interface CdsiRequestHandler { + Observable handleRequest(String username, String password, Request request, Consumer tokenSaver); + } }