mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-20 09:57:59 +01:00
Add /v1/verification
This commit is contained in:
@@ -28,9 +28,11 @@ import org.signal.registration.rpc.CheckVerificationCodeRequest;
|
||||
import org.signal.registration.rpc.CreateRegistrationSessionRequest;
|
||||
import org.signal.registration.rpc.GetRegistrationSessionMetadataRequest;
|
||||
import org.signal.registration.rpc.RegistrationServiceGrpc;
|
||||
import org.signal.registration.rpc.RegistrationSessionMetadata;
|
||||
import org.signal.registration.rpc.SendVerificationCodeRequest;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.entities.RegistrationSession;
|
||||
import org.whispersystems.textsecuregcm.controllers.VerificationSessionRateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession;
|
||||
|
||||
public class RegistrationServiceClient implements Managed {
|
||||
|
||||
@@ -76,7 +78,10 @@ public class RegistrationServiceClient implements Managed {
|
||||
this.callbackExecutor = callbackExecutor;
|
||||
}
|
||||
|
||||
public CompletableFuture<byte[]> createRegistrationSession(final Phonenumber.PhoneNumber phoneNumber, final Duration timeout) {
|
||||
// The …Session suffix methods distinguish the new methods, which return Sessions, from the old.
|
||||
// Once the deprecated methods are removed, the names can be streamlined.
|
||||
public CompletableFuture<RegistrationServiceSession> createRegistrationSessionSession(
|
||||
final Phonenumber.PhoneNumber phoneNumber, final Duration timeout) {
|
||||
final long e164 = Long.parseLong(
|
||||
PhoneNumberUtil.getInstance().format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164).substring(1));
|
||||
|
||||
@@ -85,12 +90,14 @@ public class RegistrationServiceClient implements Managed {
|
||||
.setE164(e164)
|
||||
.build()))
|
||||
.thenApply(response -> switch (response.getResponseCase()) {
|
||||
case SESSION_METADATA -> response.getSessionMetadata().getSessionId().toByteArray();
|
||||
case SESSION_METADATA -> buildSessionResponseFromMetadata(response.getSessionMetadata());
|
||||
|
||||
case ERROR -> {
|
||||
switch (response.getError().getErrorType()) {
|
||||
case CREATE_REGISTRATION_SESSION_ERROR_TYPE_RATE_LIMITED -> throw new CompletionException(
|
||||
new RateLimitExceededException(Duration.ofSeconds(response.getError().getRetryAfterSeconds()),
|
||||
new RateLimitExceededException(response.getError().getMayRetry()
|
||||
? Duration.ofSeconds(response.getError().getRetryAfterSeconds())
|
||||
: null,
|
||||
true));
|
||||
case CREATE_REGISTRATION_SESSION_ERROR_TYPE_ILLEGAL_PHONE_NUMBER -> throw new IllegalArgumentException();
|
||||
default -> throw new RuntimeException(
|
||||
@@ -102,7 +109,14 @@ public class RegistrationServiceClient implements Managed {
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<byte[]> sendRegistrationCode(final byte[] sessionId,
|
||||
@Deprecated
|
||||
public CompletableFuture<byte[]> createRegistrationSession(final Phonenumber.PhoneNumber phoneNumber,
|
||||
final Duration timeout) {
|
||||
return createRegistrationSessionSession(phoneNumber, timeout)
|
||||
.thenApply(RegistrationServiceSession::id);
|
||||
}
|
||||
|
||||
public CompletableFuture<RegistrationServiceSession> sendVerificationCode(final byte[] sessionId,
|
||||
final MessageTransport messageTransport,
|
||||
final ClientType clientType,
|
||||
@Nullable final String acceptLanguage,
|
||||
@@ -123,21 +137,57 @@ public class RegistrationServiceClient implements Managed {
|
||||
if (response.hasError()) {
|
||||
switch (response.getError().getErrorType()) {
|
||||
case SEND_VERIFICATION_CODE_ERROR_TYPE_RATE_LIMITED -> throw new CompletionException(
|
||||
new RateLimitExceededException(Duration.ofSeconds(response.getError().getRetryAfterSeconds()),
|
||||
new VerificationSessionRateLimitExceededException(
|
||||
buildSessionResponseFromMetadata(response.getSessionMetadata()),
|
||||
response.getError().getMayRetry()
|
||||
? Duration.ofSeconds(response.getError().getRetryAfterSeconds())
|
||||
: null,
|
||||
true));
|
||||
|
||||
default -> throw new CompletionException(new RuntimeException("Failed to send verification code: " + response.getError().getErrorType()));
|
||||
case SEND_VERIFICATION_CODE_ERROR_TYPE_SESSION_NOT_FOUND -> throw new CompletionException(
|
||||
new RegistrationServiceException(null));
|
||||
|
||||
case SEND_VERIFICATION_CODE_ERROR_TYPE_SESSION_ALREADY_VERIFIED -> throw new CompletionException(
|
||||
new RegistrationServiceException(buildSessionResponseFromMetadata(response.getSessionMetadata())));
|
||||
|
||||
case SEND_VERIFICATION_CODE_ERROR_TYPE_SENDER_REJECTED -> throw new CompletionException(
|
||||
RegistrationServiceSenderException.rejected(response.getError().getMayRetry()));
|
||||
case SEND_VERIFICATION_CODE_ERROR_TYPE_SENDER_ILLEGAL_ARGUMENT -> throw new CompletionException(
|
||||
RegistrationServiceSenderException.illegalArgument(response.getError().getMayRetry()));
|
||||
case SEND_VERIFICATION_CODE_ERROR_TYPE_UNSPECIFIED -> throw new CompletionException(
|
||||
RegistrationServiceSenderException.unknown(response.getError().getMayRetry()));
|
||||
|
||||
default -> throw new CompletionException(
|
||||
new RuntimeException("Failed to send verification code: " + response.getError().getErrorType()));
|
||||
}
|
||||
} else {
|
||||
return response.getSessionId().toByteArray();
|
||||
return buildSessionResponseFromMetadata(response.getSessionMetadata());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public CompletableFuture<byte[]> sendRegistrationCode(final byte[] sessionId,
|
||||
final MessageTransport messageTransport,
|
||||
final ClientType clientType,
|
||||
@Nullable final String acceptLanguage,
|
||||
final Duration timeout) {
|
||||
return sendVerificationCode(sessionId, messageTransport, clientType, acceptLanguage, timeout)
|
||||
.thenApply(RegistrationServiceSession::id);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public CompletableFuture<Boolean> checkVerificationCode(final byte[] sessionId,
|
||||
final String verificationCode,
|
||||
final Duration timeout) {
|
||||
|
||||
return checkVerificationCodeSession(sessionId, verificationCode, timeout)
|
||||
.thenApply(RegistrationServiceSession::verified);
|
||||
}
|
||||
|
||||
public CompletableFuture<RegistrationServiceSession> checkVerificationCodeSession(final byte[] sessionId,
|
||||
final String verificationCode,
|
||||
final Duration timeout) {
|
||||
return toCompletableFuture(stub.withDeadline(toDeadline(timeout))
|
||||
.checkVerificationCode(CheckVerificationCodeRequest.newBuilder()
|
||||
.setSessionId(ByteString.copyFrom(sessionId))
|
||||
@@ -147,18 +197,32 @@ public class RegistrationServiceClient implements Managed {
|
||||
if (response.hasError()) {
|
||||
switch (response.getError().getErrorType()) {
|
||||
case CHECK_VERIFICATION_CODE_ERROR_TYPE_RATE_LIMITED -> throw new CompletionException(
|
||||
new RateLimitExceededException(Duration.ofSeconds(response.getError().getRetryAfterSeconds()),
|
||||
new VerificationSessionRateLimitExceededException(
|
||||
buildSessionResponseFromMetadata(response.getSessionMetadata()),
|
||||
response.getError().getMayRetry()
|
||||
? Duration.ofSeconds(response.getError().getRetryAfterSeconds())
|
||||
: null,
|
||||
true));
|
||||
|
||||
default -> throw new CompletionException(new RuntimeException("Failed to check verification code: " + response.getError().getErrorType()));
|
||||
case CHECK_VERIFICATION_CODE_ERROR_TYPE_NO_CODE_SENT, CHECK_VERIFICATION_CODE_ERROR_TYPE_ATTEMPT_EXPIRED ->
|
||||
throw new CompletionException(
|
||||
new RegistrationServiceException(buildSessionResponseFromMetadata(response.getSessionMetadata()))
|
||||
);
|
||||
|
||||
case CHECK_VERIFICATION_CODE_ERROR_TYPE_SESSION_NOT_FOUND -> throw new CompletionException(
|
||||
new RegistrationServiceException(null)
|
||||
);
|
||||
|
||||
default -> throw new CompletionException(
|
||||
new RuntimeException("Failed to check verification code: " + response.getError().getErrorType()));
|
||||
}
|
||||
} else {
|
||||
return response.getVerified() || response.getSessionMetadata().getVerified();
|
||||
return buildSessionResponseFromMetadata(response.getSessionMetadata());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<Optional<RegistrationSession>> getSession(final byte[] sessionId,
|
||||
public CompletableFuture<Optional<RegistrationServiceSession>> getSession(final byte[] sessionId,
|
||||
final Duration timeout) {
|
||||
return toCompletableFuture(stub.withDeadline(toDeadline(timeout)).getSessionMetadata(
|
||||
GetRegistrationSessionMetadataRequest.newBuilder()
|
||||
@@ -173,11 +237,16 @@ public class RegistrationServiceClient implements Managed {
|
||||
}
|
||||
}
|
||||
|
||||
final String number = convertNumeralE164ToString(response.getSessionMetadata().getE164());
|
||||
return Optional.of(new RegistrationSession(number, response.getSessionMetadata().getVerified()));
|
||||
return Optional.of(buildSessionResponseFromMetadata(response.getSessionMetadata()));
|
||||
});
|
||||
}
|
||||
|
||||
private static RegistrationServiceSession buildSessionResponseFromMetadata(
|
||||
final RegistrationSessionMetadata sessionMetadata) {
|
||||
return new RegistrationServiceSession(sessionMetadata.getSessionId().toByteArray(),
|
||||
convertNumeralE164ToString(sessionMetadata.getE164()), sessionMetadata);
|
||||
}
|
||||
|
||||
private static Deadline toDeadline(final Duration timeout) {
|
||||
return Deadline.after(timeout.toMillis(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.registration;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession;
|
||||
|
||||
/**
|
||||
* When the Registration Service returns an error, it will also return the latest {@link RegistrationServiceSession}
|
||||
* data, so that clients may have the latest details on requesting and submitting codes.
|
||||
*/
|
||||
public class RegistrationServiceException extends Exception {
|
||||
|
||||
private final RegistrationServiceSession registrationServiceSession;
|
||||
|
||||
public RegistrationServiceException(final RegistrationServiceSession registrationServiceSession) {
|
||||
super(null, null, true, false);
|
||||
this.registrationServiceSession = registrationServiceSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if empty, the session that encountered should be considered non-existent and may be discarded
|
||||
*/
|
||||
public Optional<RegistrationServiceSession> getRegistrationSession() {
|
||||
return Optional.ofNullable(registrationServiceSession);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.registration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* An error from an SMS/voice provider (“sender”) downstream of Registration Service is mapped to a {@link Reason}, and
|
||||
* may be permanent.
|
||||
*/
|
||||
public class RegistrationServiceSenderException extends Exception {
|
||||
|
||||
private final Reason reason;
|
||||
private final boolean permanent;
|
||||
|
||||
public static RegistrationServiceSenderException illegalArgument(final boolean permanent) {
|
||||
return new RegistrationServiceSenderException(Reason.ILLEGAL_ARGUMENT, permanent);
|
||||
}
|
||||
|
||||
public static RegistrationServiceSenderException rejected(final boolean permanent) {
|
||||
return new RegistrationServiceSenderException(Reason.PROVIDER_REJECTED, permanent);
|
||||
}
|
||||
|
||||
public static RegistrationServiceSenderException unknown(final boolean permanent) {
|
||||
return new RegistrationServiceSenderException(Reason.PROVIDER_UNAVAILABLE, permanent);
|
||||
}
|
||||
|
||||
private RegistrationServiceSenderException(final Reason reason, final boolean permanent) {
|
||||
super(null, null, true, false);
|
||||
this.reason = reason;
|
||||
this.permanent = permanent;
|
||||
}
|
||||
|
||||
public Reason getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public boolean isPermanent() {
|
||||
return permanent;
|
||||
}
|
||||
|
||||
public enum Reason {
|
||||
|
||||
@JsonProperty("providerUnavailable")
|
||||
PROVIDER_UNAVAILABLE,
|
||||
@JsonProperty("providerRejected")
|
||||
PROVIDER_REJECTED,
|
||||
@JsonProperty("illegalArgument")
|
||||
ILLEGAL_ARGUMENT
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.registration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
import org.whispersystems.textsecuregcm.storage.SerializedExpireableJsonDynamoStore;
|
||||
|
||||
/**
|
||||
* Server-internal stored session object. Primarily used by
|
||||
* {@link org.whispersystems.textsecuregcm.controllers.VerificationController} to manage the steps required to begin
|
||||
* requesting codes from Registration Service, in order to get a verified session to be provided to
|
||||
* {@link org.whispersystems.textsecuregcm.controllers.RegistrationController}.
|
||||
*
|
||||
* @param pushChallenge the value of a push challenge sent to a client, after it submitted a push token
|
||||
* @param requestedInformation information requested that a client send to the server
|
||||
* @param submittedInformation information that a client has submitted and that the server has verified
|
||||
* @param allowedToRequestCode whether the client is allowed to request a code. This request will be forwarded to
|
||||
* Registration Service
|
||||
* @param createdTimestamp when this session was created
|
||||
* @param updatedTimestamp when this session was updated
|
||||
* @param remoteExpirationSeconds when the remote
|
||||
* {@link org.whispersystems.textsecuregcm.entities.RegistrationServiceSession} expires
|
||||
* @see org.whispersystems.textsecuregcm.entities.RegistrationServiceSession
|
||||
* @see org.whispersystems.textsecuregcm.entities.VerificationSessionResponse
|
||||
*/
|
||||
public record VerificationSession(@Nullable String pushChallenge,
|
||||
List<Information> requestedInformation, List<Information> submittedInformation,
|
||||
boolean allowedToRequestCode, long createdTimestamp, long updatedTimestamp,
|
||||
long remoteExpirationSeconds) implements
|
||||
SerializedExpireableJsonDynamoStore.Expireable {
|
||||
|
||||
@Override
|
||||
public long getExpirationEpochSeconds() {
|
||||
return Instant.ofEpochMilli(updatedTimestamp).plusSeconds(remoteExpirationSeconds).getEpochSecond();
|
||||
}
|
||||
|
||||
public enum Information {
|
||||
@JsonProperty("pushChallenge")
|
||||
PUSH_CHALLENGE,
|
||||
@JsonProperty("captcha")
|
||||
CAPTCHA
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user