Add /v1/verification

This commit is contained in:
Chris Eager
2023-02-22 14:27:05 -06:00
committed by GitHub
parent e1ea3795bb
commit 35286f838e
37 changed files with 3255 additions and 177 deletions

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}