From b0dc7fe6df92c35d24fc069e6f711ee4d39be5b6 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Mon, 11 Jul 2022 10:50:24 -0400 Subject: [PATCH] Add batch identity key check call for improved safety number change performance. --- app/lint.xml | 1 + dependencies.gradle | 2 +- gradle/verification-metadata.xml | 8 --- libsignal/service/build.gradle | 2 +- libsignal/service/lint.xml | 1 + .../api/SignalServiceMessageReceiver.java | 12 ++++ .../signalservice/api/SignalWebSocket.java | 2 +- .../api/services/ProfileService.java | 64 +++++++++++++++---- .../crypto/RemoteAttestationCipher.java | 12 ++-- .../internal/push/IdentityCheckRequest.java | 57 +++++++++++++++++ .../internal/push/IdentityCheckResponse.java | 41 ++++++++++++ .../internal/push/PushServiceSocket.java | 23 +++++++ .../signalservice/internal/util/JsonUtil.java | 10 ++- 13 files changed, 206 insertions(+), 29 deletions(-) create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/IdentityCheckRequest.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/IdentityCheckResponse.java diff --git a/app/lint.xml b/app/lint.xml index 09635764a..0819b090b 100644 --- a/app/lint.xml +++ b/app/lint.xml @@ -41,4 +41,5 @@ + diff --git a/dependencies.gradle b/dependencies.gradle index 0cebdabf5..94f457604 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -61,6 +61,7 @@ dependencyResolutionManagement { alias('google-zxing-android-integration').to('com.google.zxing:android-integration:3.3.0') alias('google-zxing-core').to('com.google.zxing:core:3.4.1') alias('google-ez-vcard').to('com.googlecode.ez-vcard:ez-vcard:0.9.11') + alias('google-jsr305').to('com.google.code.findbugs:jsr305:3.0.2') // Exoplayer alias('exoplayer-core').to('com.google.android.exoplayer', 'exoplayer-core').versionRef('exoplayer') @@ -86,7 +87,6 @@ dependencyResolutionManagement { alias('square-okhttp3').to('com.squareup.okhttp3:okhttp:3.12.13') alias('square-okio').to('com.squareup.okio:okio:2.2.2') alias('square-leakcanary').to('com.squareup.leakcanary:leakcanary-android:2.7') - alias('threeten-threetenbp').to('org.threeten:threetenbp:1.3.6') alias('rxjava3-rxjava').to('io.reactivex.rxjava3:rxjava:3.0.13') alias('rxjava3-rxandroid').to('io.reactivex.rxjava3:rxandroid:3.0.0') alias('rxjava3-rxkotlin').to('io.reactivex.rxjava3:rxkotlin:3.0.1') diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index eb8cb251f..dd9b58647 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -5602,14 +5602,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - diff --git a/libsignal/service/build.gradle b/libsignal/service/build.gradle index e11b388bb..2216625f7 100644 --- a/libsignal/service/build.gradle +++ b/libsignal/service/build.gradle @@ -37,7 +37,7 @@ dependencies { implementation libs.libsignal.client api libs.square.okhttp3 api libs.square.okio - implementation libs.threeten.threetenbp + implementation libs.google.jsr305 api libs.rxjava3.rxjava diff --git a/libsignal/service/lint.xml b/libsignal/service/lint.xml index 63a9d791d..77e44678b 100644 --- a/libsignal/service/lint.xml +++ b/libsignal/service/lint.xml @@ -2,4 +2,5 @@ + diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java index 18a430e00..9e36a88dc 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java @@ -23,7 +23,10 @@ import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; import org.whispersystems.signalservice.api.util.CredentialsProvider; +import org.whispersystems.signalservice.internal.ServiceResponse; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; +import org.whispersystems.signalservice.internal.push.IdentityCheckRequest; +import org.whispersystems.signalservice.internal.push.IdentityCheckResponse; import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.push.SignalServiceEnvelopeEntity; import org.whispersystems.signalservice.internal.push.SignalServiceMessagesResult; @@ -31,6 +34,7 @@ import org.whispersystems.signalservice.internal.sticker.StickerProtos; import org.whispersystems.signalservice.internal.util.Util; import org.whispersystems.signalservice.internal.util.concurrent.FutureTransformers; import org.whispersystems.signalservice.internal.util.concurrent.ListenableFuture; +import org.whispersystems.signalservice.internal.websocket.ResponseMapper; import java.io.ByteArrayOutputStream; import java.io.File; @@ -43,6 +47,10 @@ import java.util.List; import java.util.Locale; import java.util.Optional; +import javax.annotation.Nonnull; + +import io.reactivex.rxjava3.core.Single; + /** * The primary interface for receiving Signal Service messages. * @@ -130,6 +138,10 @@ public class SignalServiceMessageReceiver { return new FileInputStream(destination); } + public Single> performIdentityCheck(@Nonnull IdentityCheckRequest request, @Nonnull Optional unidentifiedAccess, @Nonnull ResponseMapper responseMapper) { + return socket.performIdentityCheck(request, unidentifiedAccess, responseMapper); + } + /** * Retrieves a SignalServiceAttachment. * diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalWebSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalWebSocket.java index 03809b501..5a98f3c4d 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalWebSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalWebSocket.java @@ -220,7 +220,7 @@ public final class SignalWebSocket { /** *

* A blocking call that reads a message off the pipe. When this call returns, the message has been - * acknowledged and will not be retransmitted. This will return {@link Optional#absent()} when an + * acknowledged and will not be retransmitted. This will return {@link Optional#empty()} when an * empty response is hit, which indicates the WebSocket is empty. *

* You can specify a {@link MessageReceivedCallback} that will be called before the received message is acknowledged. diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/ProfileService.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/ProfileService.java index 39f60fc50..dd8c5ad99 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/ProfileService.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/ProfileService.java @@ -1,5 +1,6 @@ package org.whispersystems.signalservice.api.services; +import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.util.Pair; import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; @@ -18,6 +19,9 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.MalformedResponseException; import org.whispersystems.signalservice.internal.ServiceResponse; import org.whispersystems.signalservice.internal.ServiceResponseProcessor; +import org.whispersystems.signalservice.internal.push.IdentityCheckRequest; +import org.whispersystems.signalservice.internal.push.IdentityCheckRequest.AciFingerprintPair; +import org.whispersystems.signalservice.internal.push.IdentityCheckResponse; import org.whispersystems.signalservice.internal.push.http.AcceptLanguagesUtil; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.JsonUtil; @@ -26,17 +30,25 @@ import org.whispersystems.signalservice.internal.websocket.ResponseMapper; import org.whispersystems.signalservice.internal.websocket.WebSocketProtos.WebSocketRequestMessage; import java.security.SecureRandom; +import java.util.Collections; +import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.Function; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; + +import io.reactivex.rxjava3.annotations.NonNull; import io.reactivex.rxjava3.core.Single; /** * Provide Profile-related API services, encapsulating the logic to make the request, parse the response, * and fallback to appropriate WebSocket or RESTful alternatives. */ +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") public final class ProfileService { private static final String TAG = ProfileService.class.getSimpleName(); @@ -54,11 +66,11 @@ public final class ProfileService { this.signalWebSocket = signalWebSocket; } - public Single> getProfile(SignalServiceAddress address, - Optional profileKey, - Optional unidentifiedAccess, - SignalServiceProfile.RequestType requestType, - Locale locale) + public Single> getProfile(@Nonnull SignalServiceAddress address, + @Nonnull Optional profileKey, + @Nonnull Optional unidentifiedAccess, + @Nonnull SignalServiceProfile.RequestType requestType, + @Nonnull Locale locale) { ServiceId serviceId = address.getServiceId(); SecureRandom random = new SecureRandom(); @@ -96,21 +108,51 @@ public final class ProfileService { return signalWebSocket.request(requestMessage, unidentifiedAccess) .map(responseMapper::map) - .onErrorResumeNext(t -> restFallback(address, profileKey, unidentifiedAccess, requestType, locale)) + .onErrorResumeNext(t -> getProfileRestFallback(address, profileKey, unidentifiedAccess, requestType, locale)) .onErrorReturn(ServiceResponse::forUnknownError); } - private Single> restFallback(SignalServiceAddress address, - Optional profileKey, - Optional unidentifiedAccess, - SignalServiceProfile.RequestType requestType, - Locale locale) + public @NonNull Single> performIdentityCheck(@Nonnull Map aciIdentityKeyMap, @Nonnull Optional unidentifiedAccess) { + List aciKeyPairs = aciIdentityKeyMap.entrySet() + .stream() + .map(e -> new AciFingerprintPair(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + + IdentityCheckRequest request = new IdentityCheckRequest(aciKeyPairs); + + WebSocketRequestMessage.Builder builder = WebSocketRequestMessage.newBuilder() + .setId(new SecureRandom().nextLong()) + .setVerb("POST") + .setPath("/v1/profile/identity_check/batch") + .addAllHeaders(Collections.singleton("content-type:application/json")) + .setBody(JsonUtil.toJsonByteString(request)); + + ResponseMapper responseMapper = DefaultResponseMapper.getDefault(IdentityCheckResponse.class); + + return signalWebSocket.request(builder.build(), unidentifiedAccess) + .map(responseMapper::map) + .onErrorResumeNext(t -> performIdentityCheckRestFallback(request, unidentifiedAccess, responseMapper)) + .onErrorReturn(ServiceResponse::forUnknownError); + } + + private Single> getProfileRestFallback(@Nonnull SignalServiceAddress address, + @Nonnull Optional profileKey, + @Nonnull Optional unidentifiedAccess, + @Nonnull SignalServiceProfile.RequestType requestType, + @Nonnull Locale locale) { return Single.fromFuture(receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType, locale), 10, TimeUnit.SECONDS) .onErrorResumeNext(t -> Single.fromFuture(receiver.retrieveProfile(address, profileKey, Optional.empty(), requestType, locale), 10, TimeUnit.SECONDS)) .map(p -> ServiceResponse.forResult(p, 0, null)); } + private @NonNull Single> performIdentityCheckRestFallback(@Nonnull IdentityCheckRequest request, + @Nonnull Optional unidentifiedAccess, + @Nonnull ResponseMapper responseMapper) { + return receiver.performIdentityCheck(request, unidentifiedAccess, responseMapper) + .onErrorResumeNext(t -> receiver.performIdentityCheck(request, Optional.empty(), responseMapper)); + } + /** * Maps the API {@link SignalServiceProfile} model into the desired {@link ProfileAndCredential} domain model. */ diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/RemoteAttestationCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/RemoteAttestationCipher.java index 05073edf4..f6ebd7a49 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/RemoteAttestationCipher.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/contacts/crypto/RemoteAttestationCipher.java @@ -1,12 +1,6 @@ package org.whispersystems.signalservice.internal.contacts.crypto; import org.signal.libsignal.protocol.util.ByteUtil; -import org.threeten.bp.Instant; -import org.threeten.bp.LocalDateTime; -import org.threeten.bp.Period; -import org.threeten.bp.ZoneId; -import org.threeten.bp.ZonedDateTime; -import org.threeten.bp.format.DateTimeFormatter; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse; import org.whispersystems.signalservice.internal.util.Hex; @@ -18,6 +12,12 @@ import java.security.MessageDigest; import java.security.SignatureException; import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.Period; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.HashSet; import java.util.Set; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/IdentityCheckRequest.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/IdentityCheckRequest.java new file mode 100644 index 000000000..dd964ce9f --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/IdentityCheckRequest.java @@ -0,0 +1,57 @@ +package org.whispersystems.signalservice.internal.push; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import org.signal.libsignal.protocol.IdentityKey; +import org.whispersystems.signalservice.api.push.ServiceId; +import org.whispersystems.signalservice.internal.util.JsonUtil; +import org.whispersystems.util.Base64; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; + +import javax.annotation.Nonnull; + +public class IdentityCheckRequest { + @JsonProperty("elements") + private final List aciFingerprintPairs; + + public IdentityCheckRequest(@Nonnull List aciKeyPairs) { + this.aciFingerprintPairs = aciKeyPairs; + } + + public List getAciFingerprintPairs() { + return aciFingerprintPairs; + } + + public static final class AciFingerprintPair { + + @JsonProperty + @JsonSerialize(using = JsonUtil.ServiceIdSerializer.class) + private final ServiceId aci; + + @JsonProperty + private final String fingerprint; + + public AciFingerprintPair(@Nonnull ServiceId aci, @Nonnull IdentityKey identityKey) { + this.aci = aci; + + try { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + this.fingerprint = Base64.encodeBytes(messageDigest.digest(identityKey.serialize()), 0, 4); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + + public ServiceId getAci() { + return aci; + } + + public String getFingerprint() { + return fingerprint; + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/IdentityCheckResponse.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/IdentityCheckResponse.java new file mode 100644 index 000000000..c755f841a --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/IdentityCheckResponse.java @@ -0,0 +1,41 @@ +package org.whispersystems.signalservice.internal.push; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import org.signal.libsignal.protocol.IdentityKey; +import org.whispersystems.signalservice.api.push.ServiceId; +import org.whispersystems.signalservice.internal.util.JsonUtil; + +import java.util.List; + +import javax.annotation.Nullable; + +public class IdentityCheckResponse { + + @JsonProperty("elements") + private List aciKeyPairs; + + public @Nullable List getAciKeyPairs() { + return aciKeyPairs; + } + + public static final class AciIdentityPair { + + @JsonProperty + @JsonDeserialize(using = JsonUtil.ServiceIdDeserializer.class) + private ServiceId aci; + + @JsonProperty + @JsonDeserialize(using = JsonUtil.IdentityKeyDeserializer.class) + private IdentityKey identityKey; + + public @Nullable ServiceId getAci() { + return aci; + } + + public @Nullable IdentityKey getIdentityKey() { + return identityKey; + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 3fd7ee8f2..8553fae45 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -87,6 +87,7 @@ import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.Tls12SocketFactory; import org.whispersystems.signalservice.api.util.TlsProxySocketFactory; +import org.whispersystems.signalservice.internal.ServiceResponse; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; import org.whispersystems.signalservice.internal.configuration.SignalProxy; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; @@ -124,6 +125,7 @@ import org.whispersystems.signalservice.internal.util.Util; import org.whispersystems.signalservice.internal.util.concurrent.FutureTransformers; import org.whispersystems.signalservice.internal.util.concurrent.ListenableFuture; import org.whispersystems.signalservice.internal.util.concurrent.SettableFuture; +import org.whispersystems.signalservice.internal.websocket.ResponseMapper; import org.whispersystems.util.Base64; import org.whispersystems.util.Base64UrlSafe; @@ -152,10 +154,13 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.function.Function; +import javax.annotation.Nonnull; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.schedulers.Schedulers; import okhttp3.Call; import okhttp3.Callback; import okhttp3.ConnectionPool; @@ -216,6 +221,7 @@ public class PushServiceSocket { private static final String PROFILE_PATH = "/v1/profile/%s"; private static final String PROFILE_USERNAME_PATH = "/v1/profile/username/%s"; + private static final String PROFILE_BATCH_CHECK_PATH = "/v1/profile/identity_check/batch"; private static final String SENDER_CERTIFICATE_PATH = "/v1/certificate/delivery"; private static final String SENDER_CERTIFICATE_NO_E164_PATH = "/v1/certificate/delivery?includeE164=false"; @@ -861,6 +867,23 @@ public class PushServiceSocket { return Optional.empty(); } + public Single> performIdentityCheck(@Nonnull IdentityCheckRequest request, + @Nonnull Optional unidentifiedAccess, + @Nonnull ResponseMapper responseMapper) + { + Single> requestSingle = Single.fromCallable(() -> { + Response response = getServiceConnection(PROFILE_BATCH_CHECK_PATH, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), unidentifiedAccess, false); + String body = response.body() != null ? response.body().string() : ""; + Log.e("CODY", "body: " + body); + return responseMapper.map(response.code(), body, response::header, unidentifiedAccess.isPresent()); + }); + + return requestSingle + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .onErrorReturn(ServiceResponse::forUnknownError); + } + public void setUsername(String username) throws IOException { makeServiceRequest(String.format(SET_USERNAME_PATH, username), "PUT", "", NO_HEADERS, (responseCode, body) -> { switch (responseCode) { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/util/JsonUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/util/JsonUtil.java index d8558b9df..ff4b8a6be 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/util/JsonUtil.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/util/JsonUtil.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (C) 2014-2016 Open Whisper Systems * * Licensed according to the LICENSE file in this repository. @@ -17,6 +17,7 @@ import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializerProvider; +import com.google.protobuf.ByteString; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.InvalidKeyException; @@ -30,6 +31,9 @@ import org.whispersystems.util.Base64; import java.io.IOException; import java.util.UUID; +import javax.annotation.Nonnull; + +@SuppressWarnings("unused") public class JsonUtil { private static final String TAG = JsonUtil.class.getSimpleName(); @@ -49,6 +53,10 @@ public class JsonUtil { } } + public static @Nonnull ByteString toJsonByteString(@Nonnull Object object) { + return ByteString.copyFrom(toJson(object).getBytes()); + } + public static T fromJson(String json, Class clazz) throws IOException {