kopia lustrzana https://github.com/ryukoposting/Signal-Android
214 wiersze
12 KiB
Java
214 wiersze
12 KiB
Java
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;
|
|
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
|
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
|
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest;
|
|
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequestContext;
|
|
import org.signal.libsignal.zkgroup.profiles.ProfileKeyVersion;
|
|
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
|
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.ServiceId;
|
|
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;
|
|
import org.whispersystems.signalservice.internal.websocket.DefaultResponseMapper;
|
|
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();
|
|
|
|
private final ClientZkProfileOperations clientZkProfileOperations;
|
|
private final SignalServiceMessageReceiver receiver;
|
|
private final SignalWebSocket signalWebSocket;
|
|
|
|
public ProfileService(ClientZkProfileOperations clientZkProfileOperations,
|
|
SignalServiceMessageReceiver receiver,
|
|
SignalWebSocket signalWebSocket)
|
|
{
|
|
this.clientZkProfileOperations = clientZkProfileOperations;
|
|
this.receiver = receiver;
|
|
this.signalWebSocket = signalWebSocket;
|
|
}
|
|
|
|
public Single<ServiceResponse<ProfileAndCredential>> getProfile(@Nonnull SignalServiceAddress address,
|
|
@Nonnull Optional<ProfileKey> profileKey,
|
|
@Nonnull Optional<UnidentifiedAccess> unidentifiedAccess,
|
|
@Nonnull SignalServiceProfile.RequestType requestType,
|
|
@Nonnull Locale locale)
|
|
{
|
|
ServiceId serviceId = address.getServiceId();
|
|
SecureRandom random = new SecureRandom();
|
|
ProfileKeyCredentialRequestContext requestContext = null;
|
|
|
|
WebSocketRequestMessage.Builder builder = WebSocketRequestMessage.newBuilder()
|
|
.setId(random.nextLong())
|
|
.setVerb("GET");
|
|
|
|
if (profileKey.isPresent()) {
|
|
ProfileKeyVersion profileKeyIdentifier = profileKey.get().getProfileKeyVersion(serviceId.uuid());
|
|
String version = profileKeyIdentifier.serialize();
|
|
|
|
if (requestType == SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL) {
|
|
requestContext = clientZkProfileOperations.createProfileKeyCredentialRequestContext(random, serviceId.uuid(), profileKey.get());
|
|
|
|
ProfileKeyCredentialRequest request = requestContext.getRequest();
|
|
String credentialRequest = Hex.toStringCondensed(request.serialize());
|
|
|
|
builder.setPath(String.format("/v1/profile/%s/%s/%s?credentialType=expiringProfileKey", serviceId, version, credentialRequest));
|
|
} else {
|
|
builder.setPath(String.format("/v1/profile/%s/%s", serviceId, version));
|
|
}
|
|
} else {
|
|
builder.setPath(String.format("/v1/profile/%s", address.getIdentifier()));
|
|
}
|
|
|
|
builder.addHeaders(AcceptLanguagesUtil.getAcceptLanguageHeader(locale));
|
|
|
|
WebSocketRequestMessage requestMessage = builder.build();
|
|
|
|
ResponseMapper<ProfileAndCredential> responseMapper = DefaultResponseMapper.extend(ProfileAndCredential.class)
|
|
.withResponseMapper(new ProfileResponseMapper(requestType, requestContext))
|
|
.build();
|
|
|
|
return signalWebSocket.request(requestMessage, unidentifiedAccess)
|
|
.map(responseMapper::map)
|
|
.onErrorResumeNext(t -> getProfileRestFallback(address, profileKey, unidentifiedAccess, requestType, locale))
|
|
.onErrorReturn(ServiceResponse::forUnknownError);
|
|
}
|
|
|
|
public @NonNull Single<ServiceResponse<IdentityCheckResponse>> performIdentityCheck(@Nonnull Map<ServiceId, IdentityKey> aciIdentityKeyMap, @Nonnull Optional<UnidentifiedAccess> unidentifiedAccess) {
|
|
List<AciFingerprintPair> 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<IdentityCheckResponse> 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<ServiceResponse<ProfileAndCredential>> getProfileRestFallback(@Nonnull SignalServiceAddress address,
|
|
@Nonnull Optional<ProfileKey> profileKey,
|
|
@Nonnull Optional<UnidentifiedAccess> 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<ServiceResponse<IdentityCheckResponse>> performIdentityCheckRestFallback(@Nonnull IdentityCheckRequest request,
|
|
@Nonnull Optional<UnidentifiedAccess> unidentifiedAccess,
|
|
@Nonnull ResponseMapper<IdentityCheckResponse> 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.
|
|
*/
|
|
private class ProfileResponseMapper implements DefaultResponseMapper.CustomResponseMapper<ProfileAndCredential> {
|
|
private final SignalServiceProfile.RequestType requestType;
|
|
private final ProfileKeyCredentialRequestContext requestContext;
|
|
|
|
public ProfileResponseMapper(SignalServiceProfile.RequestType requestType, ProfileKeyCredentialRequestContext requestContext) {
|
|
this.requestType = requestType;
|
|
this.requestContext = requestContext;
|
|
}
|
|
|
|
@Override
|
|
public ServiceResponse<ProfileAndCredential> map(int status, String body, Function<String, String> getHeader, boolean unidentified)
|
|
throws MalformedResponseException
|
|
{
|
|
try {
|
|
SignalServiceProfile signalServiceProfile = JsonUtil.fromJsonResponse(body, SignalServiceProfile.class);
|
|
ExpiringProfileKeyCredential expiringProfileKeyCredential = null;
|
|
if (requestContext != null && signalServiceProfile.getExpiringProfileKeyCredentialResponse() != null) {
|
|
expiringProfileKeyCredential = clientZkProfileOperations.receiveExpiringProfileKeyCredential(requestContext, signalServiceProfile.getExpiringProfileKeyCredentialResponse());
|
|
}
|
|
|
|
return ServiceResponse.forResult(new ProfileAndCredential(signalServiceProfile, requestType, Optional.ofNullable(expiringProfileKeyCredential)), status, body);
|
|
} catch (VerificationFailedException e) {
|
|
return ServiceResponse.forApplicationError(e, status, body);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Response processor for {@link ProfileAndCredential} service response.
|
|
*/
|
|
public static final class ProfileResponseProcessor extends ServiceResponseProcessor<ProfileAndCredential> {
|
|
public ProfileResponseProcessor(ServiceResponse<ProfileAndCredential> response) {
|
|
super(response);
|
|
}
|
|
|
|
public <T> Pair<T, ProfileAndCredential> getResult(T with) {
|
|
return new Pair<>(with, getResult());
|
|
}
|
|
|
|
@Override
|
|
public boolean notFound() {
|
|
return super.notFound();
|
|
}
|
|
|
|
@Override
|
|
public boolean genericIoError() {
|
|
return super.genericIoError();
|
|
}
|
|
|
|
@Override
|
|
public Throwable getError() {
|
|
return super.getError();
|
|
}
|
|
}
|
|
}
|