From 3c08b070fc19cc11ee9e21ca6007b45eae188424 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Mon, 16 May 2022 14:56:48 -0400 Subject: [PATCH] Fetch PNI Credential during own profile refresh. --- .../sync/ContactDiscoveryRefreshV1.java | 6 +- .../dependencies/ApplicationDependencies.java | 17 ++++ .../ApplicationDependencyProvider.java | 10 +++ .../securesms/jobs/RefreshOwnProfileJob.java | 13 +++ .../securesms/jobs/RetrieveProfileJob.java | 6 +- .../securesms/keyvalue/AccountValues.kt | 6 ++ .../securesms/util/ProfileUtil.java | 13 +-- .../MockApplicationDependencyProvider.java | 7 ++ .../api/SignalServiceMessageReceiver.java | 5 ++ .../api/profiles/SignalServiceProfile.java | 19 ++++ .../api/services/ProfileService.java | 87 +++++++++++++++++-- .../internal/ServiceResponseProcessor.java | 6 ++ .../internal/push/PushServiceSocket.java | 15 ++++ 13 files changed, 186 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV1.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV1.java index df28acaa0..57af36cc4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV1.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV1.java @@ -184,12 +184,8 @@ class ContactDiscoveryRefreshV1 { .filter(ContactDiscoveryRefreshV1::hasCommunicatedWith) .toList(); - ProfileService profileService = new ProfileService(ApplicationDependencies.getGroupsV2Operations().getProfileOperations(), - ApplicationDependencies.getSignalServiceMessageReceiver(), - ApplicationDependencies.getSignalWebSocket()); - List>>> requests = Stream.of(possiblyUnlisted) - .map(r -> ProfileUtil.retrieveProfile(context, r, SignalServiceProfile.RequestType.PROFILE, profileService) + .map(r -> ProfileUtil.retrieveProfile(context, r, SignalServiceProfile.RequestType.PROFILE) .toObservable() .timeout(5, TimeUnit.SECONDS) .onErrorReturn(t -> new Pair<>(r, ServiceResponse.forUnknownError(t)))) diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java index ac936a493..cf888f749 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -8,6 +8,7 @@ import androidx.annotation.VisibleForTesting; import org.signal.core.util.Hex; import org.signal.core.util.concurrent.DeadlockDetector; +import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations; import org.thoughtcrime.securesms.KbsEnclave; import org.thoughtcrime.securesms.components.TypingStatusRepository; @@ -53,6 +54,7 @@ import org.whispersystems.signalservice.api.SignalWebSocket; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.api.services.DonationsService; +import org.whispersystems.signalservice.api.services.ProfileService; import org.whispersystems.signalservice.api.util.Tls12SocketFactory; import org.whispersystems.signalservice.internal.util.BlacklistingTrustManager; import org.whispersystems.signalservice.internal.util.Util; @@ -120,6 +122,7 @@ public class ApplicationDependencies { private static volatile SimpleExoPlayerPool exoPlayerPool; private static volatile AudioManagerCompat audioManagerCompat; private static volatile DonationsService donationsService; + private static volatile ProfileService profileService; private static volatile DeadlockDetector deadlockDetector; private static volatile ClientZkReceiptOperations clientZkReceiptOperations; @@ -632,6 +635,19 @@ public class ApplicationDependencies { return donationsService; } + public static @NonNull ProfileService getProfileService() { + if (profileService == null) { + synchronized (LOCK) { + if (profileService == null) { + profileService = provider.provideProfileService(ApplicationDependencies.getGroupsV2Operations().getProfileOperations(), + ApplicationDependencies.getSignalServiceMessageReceiver(), + ApplicationDependencies.getSignalWebSocket()); + } + } + } + return profileService; + } + public static @NonNull ClientZkReceiptOperations getClientZkReceiptOperations() { if (clientZkReceiptOperations == null) { synchronized (LOCK) { @@ -688,6 +704,7 @@ public class ApplicationDependencies { @NonNull SimpleExoPlayerPool provideExoPlayerPool(); @NonNull AudioManagerCompat provideAndroidCallAudioManager(); @NonNull DonationsService provideDonationsService(); + @NonNull ProfileService provideProfileService(@NonNull ClientZkProfileOperations profileOperations, @NonNull SignalServiceMessageReceiver signalServiceMessageReceiver, @NonNull SignalWebSocket signalWebSocket); @NonNull DeadlockDetector provideDeadlockDetector(); @NonNull ClientZkReceiptOperations provideClientZkReceiptOperations(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index 965247b6e..a4f35e765 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import org.signal.core.util.concurrent.DeadlockDetector; import org.signal.core.util.concurrent.SignalExecutors; +import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations; import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.components.TypingStatusRepository; @@ -79,6 +80,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.services.DonationsService; +import org.whispersystems.signalservice.api.services.ProfileService; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; @@ -352,6 +354,14 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr FeatureFlags.okHttpAutomaticRetry()); } + @Override + public @NonNull ProfileService provideProfileService(@NonNull ClientZkProfileOperations clientZkProfileOperations, + @NonNull SignalServiceMessageReceiver receiver, + @NonNull SignalWebSocket signalWebSocket) + { + return new ProfileService(clientZkProfileOperations, receiver, signalWebSocket); + } + @Override public @NonNull DeadlockDetector provideDeadlockDetector() { HandlerThread handlerThread = new HandlerThread("signal-DeadlockDetector"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java index 9e070dc46..90933e0a3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java @@ -6,6 +6,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.signal.core.util.logging.Log; +import org.signal.libsignal.zkgroup.profiles.PniCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential; import org.thoughtcrime.securesms.badges.BadgeRepository; @@ -33,6 +34,7 @@ import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription; import org.whispersystems.signalservice.internal.ServiceResponse; +import org.whispersystems.signalservice.internal.ServiceResponseProcessor; import java.io.IOException; import java.util.Comparator; @@ -134,6 +136,17 @@ public class RefreshOwnProfileJob extends BaseJob { if (profileKeyCredential.isPresent()) { setProfileKeyCredential(self, ProfileKeyUtil.getSelfProfileKey(), profileKeyCredential.get()); } + + if (SignalStore.account().getAci() != null) { + PniCredential pniCredential = ApplicationDependencies.getProfileService() + .getPniProfileCredential(SignalStore.account().requireAci(), + SignalStore.account().requirePni(), + ProfileKeyUtil.getSelfProfileKey()) + .map(ServiceResponseProcessor.DefaultProcessor::new) + .blockingGet() + .getResultOrThrow(); + SignalStore.account().setPniCredential(pniCredential); + } } private void setProfileKeyCredential(@NonNull Recipient recipient, diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java index 3c13ace23..829357af2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java @@ -253,13 +253,9 @@ public class RetrieveProfileJob extends BaseJob { List recipients = Recipient.resolvedList(recipientIds); stopwatch.split("resolve-ensure"); - ProfileService profileService = new ProfileService(ApplicationDependencies.getGroupsV2Operations().getProfileOperations(), - ApplicationDependencies.getSignalServiceMessageReceiver(), - ApplicationDependencies.getSignalWebSocket()); - List>>> requests = Stream.of(recipients) .filter(Recipient::hasServiceId) - .map(r -> ProfileUtil.retrieveProfile(context, r, getRequestType(r), profileService).toObservable()) + .map(r -> ProfileUtil.retrieveProfile(context, r, getRequestType(r)).toObservable()) .toList(); stopwatch.split("requests"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt index 46517665d..2eed9e6ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt @@ -9,6 +9,7 @@ import org.signal.libsignal.protocol.IdentityKey import org.signal.libsignal.protocol.IdentityKeyPair import org.signal.libsignal.protocol.ecc.Curve import org.signal.libsignal.protocol.util.Medium +import org.signal.libsignal.zkgroup.profiles.PniCredential import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.MasterCipher import org.thoughtcrime.securesms.crypto.ProfileKeyUtil @@ -53,6 +54,7 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal private const val KEY_PNI_ACTIVE_SIGNED_PREKEY_ID = "account.pni_active_signed_prekey_id" private const val KEY_PNI_SIGNED_PREKEY_FAILURE_COUNT = "account.pni_signed_prekey_failure_count" private const val KEY_PNI_NEXT_ONE_TIME_PREKEY_ID = "account.pni_next_one_time_prekey_id" + private const val KEY_PNI_CREDENTIAL = "account.pni_credential" @VisibleForTesting const val KEY_E164 = "account.e164" @@ -305,6 +307,10 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal val isLinkedDevice: Boolean get() = !isPrimaryDevice + var pniCredential: PniCredential? + set(value) = putBlob(KEY_PNI_CREDENTIAL, value?.serialize()) + get() = getBlob(KEY_PNI_CREDENTIAL, null)?.let { PniCredential(it) } + private fun clearLocalCredentials(context: Context) { putString(KEY_SERVICE_PASSWORD, Util.getSecret(18)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java index 31ca84875..e6fb8ea93 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java @@ -102,28 +102,23 @@ public final class ProfileUtil { boolean allowUnidentifiedAccess) throws IOException { - ProfileService profileService = new ProfileService(ApplicationDependencies.getGroupsV2Operations().getProfileOperations(), - ApplicationDependencies.getSignalServiceMessageReceiver(), - ApplicationDependencies.getSignalWebSocket()); - - Pair> response = retrieveProfile(context, recipient, requestType, profileService, allowUnidentifiedAccess).blockingGet(); + Pair> response = retrieveProfile(context, recipient, requestType, allowUnidentifiedAccess).blockingGet(); return new ProfileService.ProfileResponseProcessor(response.second()).getResultOrThrow(); } public static Single>> retrieveProfile(@NonNull Context context, @NonNull Recipient recipient, - @NonNull SignalServiceProfile.RequestType requestType, - @NonNull ProfileService profileService) + @NonNull SignalServiceProfile.RequestType requestType) { - return retrieveProfile(context, recipient, requestType, profileService, true); + return retrieveProfile(context, recipient, requestType, true); } private static Single>> retrieveProfile(@NonNull Context context, @NonNull Recipient recipient, @NonNull SignalServiceProfile.RequestType requestType, - @NonNull ProfileService profileService, boolean allowUnidentifiedAccess) { + ProfileService profileService = ApplicationDependencies.getProfileService(); Optional unidentifiedAccess = allowUnidentifiedAccess ? getUnidentifiedAccess(context, recipient) : Optional.empty(); Optional profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey()); diff --git a/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.java b/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.java index 374a642ef..8b2802b16 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.java +++ b/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.java @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.dependencies; import androidx.annotation.NonNull; import org.signal.core.util.concurrent.DeadlockDetector; +import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations; import org.thoughtcrime.securesms.components.TypingStatusRepository; import org.thoughtcrime.securesms.components.TypingStatusSender; @@ -38,6 +39,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalWebSocket; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.services.DonationsService; +import org.whispersystems.signalservice.api.services.ProfileService; import static org.mockito.Mockito.mock; @@ -207,6 +209,11 @@ public class MockApplicationDependencyProvider implements ApplicationDependencie return null; } + @Override + public @NonNull ProfileService provideProfileService(@NonNull ClientZkProfileOperations profileOperations, @NonNull SignalServiceMessageReceiver signalServiceMessageReceiver, @NonNull SignalWebSocket signalWebSocket) { + return null; + } + @Override public @NonNull DeadlockDetector provideDeadlockDetector() { return null; 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..aa2c9822c 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 @@ -19,6 +19,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; +import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; @@ -110,6 +111,10 @@ public class SignalServiceMessageReceiver { } } + public ListenableFuture retrievePniProfile(ACI aci, String version, String credentialRequest, Locale locale) { + return socket.retrievePniCredential(aci.uuid(), version, credentialRequest, locale); + } + public SignalServiceProfile retrieveProfileByUsername(String username, Optional unidentifiedAccess, Locale locale) throws IOException { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java index ce71132be..809be09bd 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.signal.libsignal.protocol.logging.Log; import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.profiles.PniCredentialResponse; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialResponse; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.internal.util.JsonUtil; @@ -63,6 +64,9 @@ public class SignalServiceProfile { @JsonProperty private List badges; + @JsonProperty + private byte[] pniCredential; + @JsonIgnore private RequestType requestType; @@ -116,6 +120,10 @@ public class SignalServiceProfile { return requestType; } + public byte[] getPniCredential() { + return pniCredential; + } + public void setRequestType(RequestType requestType) { this.requestType = requestType; } @@ -255,4 +263,15 @@ public class SignalServiceProfile { return null; } } + + public PniCredentialResponse getPniCredentialResponse() { + if (pniCredential == null) return null; + + try { + return new PniCredentialResponse(pniCredential); + } catch (InvalidInputException e) { + Log.w(TAG, e); + return null; + } + } } 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 5e89837f8..858e36d06 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 @@ -3,6 +3,8 @@ package org.whispersystems.signalservice.api.services; import org.signal.libsignal.protocol.util.Pair; import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; +import org.signal.libsignal.zkgroup.profiles.PniCredential; +import org.signal.libsignal.zkgroup.profiles.PniCredentialRequestContext; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest; @@ -13,6 +15,8 @@ import org.whispersystems.signalservice.api.SignalWebSocket; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; +import org.whispersystems.signalservice.api.push.ACI; +import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.MalformedResponseException; @@ -23,7 +27,7 @@ import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.JsonUtil; import org.whispersystems.signalservice.internal.websocket.DefaultResponseMapper; import org.whispersystems.signalservice.internal.websocket.ResponseMapper; -import org.whispersystems.signalservice.internal.websocket.WebSocketProtos; +import org.whispersystems.signalservice.internal.websocket.WebSocketProtos.WebSocketRequestMessage; import java.security.SecureRandom; import java.util.Locale; @@ -31,7 +35,10 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.Function; +import io.reactivex.rxjava3.core.Scheduler; import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.core.SingleSource; +import io.reactivex.rxjava3.schedulers.Schedulers; /** * Provide Profile-related API services, encapsulating the logic to make the request, parse the response, @@ -64,9 +71,9 @@ public final class ProfileService { SecureRandom random = new SecureRandom(); ProfileKeyCredentialRequestContext requestContext = null; - WebSocketProtos.WebSocketRequestMessage.Builder builder = WebSocketProtos.WebSocketRequestMessage.newBuilder() - .setId(random.nextLong()) - .setVerb("GET"); + WebSocketRequestMessage.Builder builder = WebSocketRequestMessage.newBuilder() + .setId(random.nextLong()) + .setVerb("GET"); if (profileKey.isPresent()) { ProfileKeyVersion profileKeyIdentifier = profileKey.get().getProfileKeyVersion(serviceId.uuid()); @@ -88,7 +95,7 @@ public final class ProfileService { builder.addHeaders(AcceptLanguagesUtil.getAcceptLanguageHeader(locale)); - WebSocketProtos.WebSocketRequestMessage requestMessage = builder.build(); + WebSocketRequestMessage requestMessage = builder.build(); ResponseMapper responseMapper = DefaultResponseMapper.extend(ProfileAndCredential.class) .withResponseMapper(new ProfileResponseMapper(requestType, requestContext)) @@ -111,6 +118,40 @@ public final class ProfileService { .map(p -> ServiceResponse.forResult(p, 0, null)); } + public Single> getPniProfileCredential(ACI aci, + PNI pni, + ProfileKey profileKey) + { + SecureRandom random = new SecureRandom(); + ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion(aci.uuid()); + String version = profileKeyIdentifier.serialize(); + PniCredentialRequestContext requestContext = clientZkProfileOperations.createPniCredentialRequestContext(random, aci.uuid(), pni.uuid(), profileKey); + ProfileKeyCredentialRequest request = requestContext.getRequest(); + String credentialRequest = Hex.toStringCondensed(request.serialize()); + + WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder() + .setId(random.nextLong()) + .setVerb("GET") + .setPath(String.format("/v1/profile/%s/%s/%s?credentialType=pni", aci.uuid(), version, credentialRequest)) + .addHeaders(AcceptLanguagesUtil.getAcceptLanguageHeader(Locale.getDefault())) + .build(); + + PniCredentialMapper pniCredentialMapper = new PniCredentialMapper(requestContext); + ResponseMapper responseMapper = DefaultResponseMapper.extend(PniCredential.class) + .withResponseMapper(pniCredentialMapper) + .build(); + + return signalWebSocket.request(requestMessage, Optional.empty()) + .map(responseMapper::map) + .onErrorResumeNext(t -> restFallbackForPni(pniCredentialMapper, aci, version, credentialRequest, Locale.getDefault())) + .onErrorReturn(ServiceResponse::forUnknownError); + } + + private Single> restFallbackForPni(PniCredentialMapper responseMapper, ACI aci, String version, String credentialRequest, Locale locale) { + return Single.fromFuture(receiver.retrievePniProfile(aci, version, credentialRequest, locale), 10, TimeUnit.SECONDS) + .map(responseMapper::map); + } + /** * Maps the API {@link SignalServiceProfile} model into the desired {@link ProfileAndCredential} domain model. */ @@ -141,6 +182,42 @@ public final class ProfileService { } } + /** + * Maps the API {@link SignalServiceProfile} model into the desired {@link org.signal.libsignal.zkgroup.profiles.PniCredential} domain model. + */ + private class PniCredentialMapper implements DefaultResponseMapper.CustomResponseMapper { + private final PniCredentialRequestContext requestContext; + + public PniCredentialMapper(PniCredentialRequestContext requestContext) { + this.requestContext = requestContext; + } + + @Override + public ServiceResponse map(int status, String body, Function getHeader, boolean unidentified) + throws MalformedResponseException + { + SignalServiceProfile signalServiceProfile = JsonUtil.fromJsonResponse(body, SignalServiceProfile.class); + return map(signalServiceProfile); + } + + public ServiceResponse map(SignalServiceProfile signalServiceProfile) { + try { + PniCredential pniCredential = null; + if (requestContext != null && signalServiceProfile.getPniCredentialResponse() != null) { + pniCredential = clientZkProfileOperations.receivePniCredential(requestContext, signalServiceProfile.getPniCredentialResponse()); + } + + if (pniCredential == null) { + return ServiceResponse.forApplicationError(new MalformedResponseException("No PNI credential in response"), 0, null); + } else { + return ServiceResponse.forResult(pniCredential, 200, null); + } + } catch (VerificationFailedException e) { + return ServiceResponse.forUnknownError(e); + } + } + } + /** * Response processor for {@link ProfileAndCredential} service response. */ diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/ServiceResponseProcessor.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/ServiceResponseProcessor.java index d9a3cd45b..08c0cfd52 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/ServiceResponseProcessor.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/ServiceResponseProcessor.java @@ -126,4 +126,10 @@ public abstract class ServiceResponseProcessor { error instanceof TimeoutException || error instanceof InterruptedException; } + + public static final class DefaultProcessor extends ServiceResponseProcessor { + public DefaultProcessor(ServiceResponse response) { + super(response); + } + } } 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 9dabfc5a0..06b246891 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 @@ -22,6 +22,7 @@ import org.signal.libsignal.protocol.state.SignedPreKeyRecord; import org.signal.libsignal.protocol.util.Pair; import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; +import org.signal.libsignal.zkgroup.profiles.PniCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest; @@ -814,6 +815,20 @@ public class PushServiceSocket { }); } + public ListenableFuture retrievePniCredential(UUID target, String version, String credentialRequest, Locale locale) { + String subPath = String.format("%s/%s/%s?credentialType=pni", target, version, credentialRequest); + ListenableFuture response = submitServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), Optional.empty()); + + return FutureTransformers.map(response, body -> { + try { + return JsonUtil.fromJson(body, SignalServiceProfile.class); + } catch (IOException e) { + Log.w(TAG, e); + throw new MalformedResponseException("Unable to parse entity", e); + } + }); + } + public void retrieveProfileAvatar(String path, File destination, long maxSizeBytes) throws IOException {