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
{