Add libsignal-net CDSI implementation.

This commit is contained in:
Alex Konradi 2024-03-01 14:45:37 -05:00 committed by Alex Hart
parent 46c8b3b690
commit 56eae8c7bf
5 changed files with 66 additions and 12 deletions

View file

@ -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\"")

View file

@ -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) {

View file

@ -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<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

View file

@ -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<byte[]> token,
String mrEnclave,
Long timeoutMs,
@Nullable Network.Environment libsignalNetEnv,
Consumer<byte[]> 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<ServiceResponse<CdsiV2Service.Response>> single = service.getRegisteredUsers(auth.getUsername(), auth.getPassword(), request, tokenSaver);

View file

@ -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<CdsiLookupResponse> 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<ServiceResponse<Response>> getRegisteredUsers(String username, String password, Request request, Consumer<byte[]> 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<String, ResponseItem> all = new HashMap<>();
@ -139,6 +165,18 @@ public final class CdsiV2Service {
return ByteString.of(os.toByteArray());
}
private static CdsiLookupRequest buildLibsignalRequest(Request request) {
HashMap<org.signal.libsignal.protocol.ServiceId, ProfileKey> 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<String, ResponseItem> 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<Long> parseAndSortE164Strings(Collection<String> e164s) {
return e164s.stream()
.map(Long::parseLong)
@ -216,4 +254,8 @@ public final class CdsiV2Service {
return aci.isPresent();
}
}
private interface CdsiRequestHandler {
Observable<Response> handleRequest(String username, String password, Request request, Consumer<byte[]> tokenSaver);
}
}