Add experiment to test standalone registration service

This commit is contained in:
Jon Chambers
2022-10-06 15:42:53 -04:00
committed by GitHub
parent d6c9652a70
commit d2fa00f0c6
19 changed files with 705 additions and 136 deletions

View File

@@ -37,6 +37,7 @@ import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
import org.whispersystems.textsecuregcm.configuration.RecaptchaConfiguration;
import org.whispersystems.textsecuregcm.configuration.RedisClusterConfiguration;
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
import org.whispersystems.textsecuregcm.configuration.RegistrationServiceConfiguration;
import org.whispersystems.textsecuregcm.configuration.RemoteConfigConfiguration;
import org.whispersystems.textsecuregcm.configuration.ReportMessageConfiguration;
import org.whispersystems.textsecuregcm.configuration.SecureBackupServiceConfiguration;
@@ -263,6 +264,11 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private AbusiveMessageFilterConfiguration abusiveMessageFilter;
@Valid
@NotNull
@JsonProperty
private RegistrationServiceConfiguration registrationService;
public AdminEventLoggingConfiguration getAdminEventLoggingConfiguration() {
return adminEventLoggingConfiguration;
}
@@ -444,4 +450,8 @@ public class WhisperServerConfiguration extends Configuration {
public UsernameConfiguration getUsername() {
return username;
}
public RegistrationServiceConfiguration getRegistrationServiceConfiguration() {
return registrationService;
}
}

View File

@@ -158,6 +158,7 @@ import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
import org.whispersystems.textsecuregcm.redis.ConnectionEventLogger;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
import org.whispersystems.textsecuregcm.s3.PolicySigner;
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
@@ -410,6 +411,11 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
.workQueue(receiptSenderQueue)
.rejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy())
.build();
ExecutorService registrationCallbackExecutor = environment.lifecycle()
.executorService(name(getClass(), "registration-%d"))
.maxThreads(2)
.minThreads(2)
.build();
final AdminEventLogger adminEventLogger = new GoogleCloudAdminEventLogger(
LoggingOptions.newBuilder().setProjectId(config.getAdminEventLoggingConfiguration().projectId())
@@ -445,6 +451,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
config.getPaymentsServiceConfiguration().getUserAuthenticationTokenSharedSecret(), true);
AbusiveHostRules abusiveHostRules = new AbusiveHostRules(rateLimitersCluster, dynamicConfigurationManager);
RegistrationServiceClient registrationServiceClient = new RegistrationServiceClient(config.getRegistrationServiceConfiguration().getHost(), config.getRegistrationServiceConfiguration().getPort(), config.getRegistrationServiceConfiguration().getApiKey(), config.getRegistrationServiceConfiguration().getRegistrationCaCertificate(), registrationCallbackExecutor);
SecureBackupClient secureBackupClient = new SecureBackupClient(backupCredentialsGenerator, backupServiceExecutor, config.getSecureBackupServiceConfiguration());
SecureStorageClient secureStorageClient = new SecureStorageClient(storageCredentialsGenerator, storageServiceExecutor, config.getSecureStorageServiceConfiguration());
ClientPresenceManager clientPresenceManager = new ClientPresenceManager(clientPresenceCluster, recurringJobExecutor, keyspaceNotificationDispatchExecutor);
@@ -581,6 +588,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.lifecycle().manage(clientPresenceManager);
environment.lifecycle().manage(currencyManager);
environment.lifecycle().manage(directoryQueue);
environment.lifecycle().manage(registrationServiceClient);
StaticCredentialsProvider cdnCredentialsProvider = StaticCredentialsProvider
.create(AwsBasicCredentials.create(
@@ -638,9 +646,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
// these should be common, but use @Auth DisabledPermittedAccount, which isnt supported yet on websocket
environment.jersey().register(
new AccountController(pendingAccountsManager, accountsManager, abusiveHostRules, rateLimiters,
smsSender, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(),
smsSender, registrationServiceClient, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(),
recaptchaClient, pushNotificationManager, verifyExperimentEnrollmentManager,
changeNumberManager, backupCredentialsGenerator));
changeNumberManager, backupCredentialsGenerator, experimentEnrollmentManager));
environment.jersey().register(new KeysController(rateLimiters, keys, accountsManager));
final List<Object> commonControllers = Lists.newArrayList(

View File

@@ -5,60 +5,19 @@
package org.whispersystems.textsecuregcm.auth;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.security.MessageDigest;
import java.time.Duration;
import java.util.Optional;
import javax.annotation.Nullable;
import org.whispersystems.textsecuregcm.util.Util;
public class StoredVerificationCode {
@JsonProperty
private final String code;
@JsonProperty
private final long timestamp;
@JsonProperty
private final String pushCode;
@JsonProperty
@Nullable
private final String twilioVerificationSid;
public record StoredVerificationCode(String code,
long timestamp,
String pushCode,
@Nullable String twilioVerificationSid,
@Nullable byte[] sessionId) {
public static final Duration EXPIRATION = Duration.ofMinutes(10);
@JsonCreator
public StoredVerificationCode(
@JsonProperty("code") final String code,
@JsonProperty("timestamp") final long timestamp,
@JsonProperty("pushCode") final String pushCode,
@JsonProperty("twilioVerificationSid") @Nullable final String twilioVerificationSid) {
this.code = code;
this.timestamp = timestamp;
this.pushCode = pushCode;
this.twilioVerificationSid = twilioVerificationSid;
}
public String getCode() {
return code;
}
public long getTimestamp() {
return timestamp;
}
public String getPushCode() {
return pushCode;
}
public Optional<String> getTwilioVerificationSid() {
return Optional.ofNullable(twilioVerificationSid);
}
public boolean isValid(String theirCodeString) {
if (Util.isEmpty(code) || Util.isEmpty(theirCodeString)) {
return false;

View File

@@ -0,0 +1,49 @@
package org.whispersystems.textsecuregcm.configuration;
import javax.validation.constraints.NotBlank;
public class RegistrationServiceConfiguration {
@NotBlank
private String host;
private int port = 443;
@NotBlank
private String apiKey;
@NotBlank
private String registrationCaCertificate;
public String getHost() {
return host;
}
public void setHost(final String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(final int port) {
this.port = port;
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(final String apiKey) {
this.apiKey = apiKey;
}
public String getRegistrationCaCertificate() {
return registrationCaCertificate;
}
public void setRegistrationCaCertificate(final String registrationCaCertificate) {
this.registrationCaCertificate = registrationCaCertificate;
}
}

View File

@@ -11,6 +11,9 @@ import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.annotation.Timed;
import com.google.common.annotations.VisibleForTesting;
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
@@ -80,12 +83,16 @@ import org.whispersystems.textsecuregcm.entities.ReserveUsernameResponse;
import org.whispersystems.textsecuregcm.entities.StaleDevices;
import org.whispersystems.textsecuregcm.entities.UsernameRequest;
import org.whispersystems.textsecuregcm.entities.UsernameResponse;
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
import org.whispersystems.textsecuregcm.push.PushNotification;
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
import org.whispersystems.textsecuregcm.registration.ClientType;
import org.whispersystems.textsecuregcm.registration.MessageTransport;
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.storage.AbusiveHostRules;
@@ -145,14 +152,13 @@ public class AccountController {
private static final String VERIFICATION_TRANSPORT_TAG_NAME = "transport";
private static final String SCORE_TAG_NAME = "score";
private static final String VERIFY_EXPERIMENT_TAG_NAME = "twilioVerify";
private final StoredVerificationCodeManager pendingAccounts;
private final AccountsManager accounts;
private final AbusiveHostRules abusiveHostRules;
private final RateLimiters rateLimiters;
private final SmsSender smsSender;
private final RegistrationServiceClient registrationServiceClient;
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
private final TurnTokenGenerator turnTokenGenerator;
private final Map<String, Integer> testDevices;
@@ -161,13 +167,21 @@ public class AccountController {
private final ExternalServiceCredentialGenerator backupServiceCredentialGenerator;
private final TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager;
private final ExperimentEnrollmentManager experimentEnrollmentManager;
private final ChangeNumberManager changeNumberManager;
@VisibleForTesting
static final String REGISTRATION_SERVICE_EXPERIMENT_NAME = "registration-service";
@VisibleForTesting
static final Duration REGISTRATION_RPC_TIMEOUT = Duration.ofSeconds(15);
public AccountController(StoredVerificationCodeManager pendingAccounts,
AccountsManager accounts,
AbusiveHostRules abusiveHostRules,
RateLimiters rateLimiters,
SmsSender smsSenderFactory,
RegistrationServiceClient registrationServiceClient,
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
TurnTokenGenerator turnTokenGenerator,
Map<String, Integer> testDevices,
@@ -175,13 +189,15 @@ public class AccountController {
PushNotificationManager pushNotificationManager,
TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager,
ChangeNumberManager changeNumberManager,
ExternalServiceCredentialGenerator backupServiceCredentialGenerator)
ExternalServiceCredentialGenerator backupServiceCredentialGenerator,
final ExperimentEnrollmentManager experimentEnrollmentManager)
{
this.pendingAccounts = pendingAccounts;
this.accounts = accounts;
this.abusiveHostRules = abusiveHostRules;
this.rateLimiters = rateLimiters;
this.smsSender = smsSenderFactory;
this.registrationServiceClient = registrationServiceClient;
this.dynamicConfigurationManager = dynamicConfigurationManager;
this.testDevices = testDevices;
this.turnTokenGenerator = turnTokenGenerator;
@@ -190,6 +206,7 @@ public class AccountController {
this.verifyExperimentEnrollmentManager = verifyExperimentEnrollmentManager;
this.backupServiceCredentialGenerator = backupServiceCredentialGenerator;
this.changeNumberManager = changeNumberManager;
this.experimentEnrollmentManager = experimentEnrollmentManager;
}
@Timed
@@ -210,14 +227,12 @@ public class AccountController {
Util.requireNormalizedNumber(number);
String pushChallenge = generatePushChallenge();
StoredVerificationCode storedVerificationCode = new StoredVerificationCode(null,
System.currentTimeMillis(),
pushChallenge,
null);
String pushChallenge = generatePushChallenge();
StoredVerificationCode storedVerificationCode =
new StoredVerificationCode(null, System.currentTimeMillis(), pushChallenge, null, null);
pendingAccounts.store(number, storedVerificationCode);
pushNotificationManager.sendRegistrationChallengeNotification(pushToken, tokenType, storedVerificationCode.getPushCode());
pushNotificationManager.sendRegistrationChallengeNotification(pushToken, tokenType, storedVerificationCode.pushCode());
return Response.ok().build();
}
@@ -239,9 +254,8 @@ public class AccountController {
Util.requireNormalizedNumber(number);
String sourceHost = ForwardedIpUtil.getMostRecentProxy(forwardedFor).orElseThrow();
Optional<StoredVerificationCode> storedChallenge = pendingAccounts.getCodeForNumber(number);
final String sourceHost = ForwardedIpUtil.getMostRecentProxy(forwardedFor).orElseThrow();
final Optional<StoredVerificationCode> maybeStoredVerificationCode = pendingAccounts.getCodeForNumber(number);
final String countryCode = Util.getCountryCode(number);
final String region = Util.getRegion(number);
@@ -260,7 +274,8 @@ public class AccountController {
Tag.of(SCORE_TAG_NAME, result.score())))
.increment());
boolean pushChallengeMatch = pushChallengeMatches(number, pushChallenge, storedChallenge);
final boolean pushChallengeMatch = pushChallengeMatches(number, pushChallenge, maybeStoredVerificationCode);
if (pushChallenge.isPresent() && !pushChallengeMatch) {
throw new WebApplicationException(Response.status(403).build());
}
@@ -289,11 +304,46 @@ public class AccountController {
default -> throw new WebApplicationException(Response.status(422).build());
}
VerificationCode verificationCode = generateVerificationCode(number);
StoredVerificationCode storedVerificationCode = new StoredVerificationCode(verificationCode.getVerificationCode(),
if (experimentEnrollmentManager.isEnrolled(number, REGISTRATION_SERVICE_EXPERIMENT_NAME)) {
sendVerificationCodeViaRegistrationService(number,
maybeStoredVerificationCode,
acceptLanguage,
client,
transport);
} else {
sendVerificationCodeViaTwilioSender(number,
maybeStoredVerificationCode,
acceptLanguage,
userAgent,
client,
transport,
assessmentResult);
}
Metrics.counter(ACCOUNT_CREATE_COUNTER_NAME, Tags.of(
UserAgentTagUtil.getPlatformTag(userAgent),
Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)),
Tag.of(REGION_TAG_NAME, Util.getRegion(number)),
Tag.of(VERIFICATION_TRANSPORT_TAG_NAME, transport)))
.increment();
return Response.ok().build();
}
private void sendVerificationCodeViaTwilioSender(final String number,
final Optional<StoredVerificationCode> maybeStoredVerificationCode,
final Optional<String> acceptLanguage,
final String userAgent,
final Optional<String> client,
final String transport,
final Optional<RecaptchaClient.AssessmentResult> assessmentResult) {
final VerificationCode verificationCode = generateVerificationCode(number);
final StoredVerificationCode storedVerificationCode = new StoredVerificationCode(verificationCode.getVerificationCode(),
System.currentTimeMillis(),
storedChallenge.map(StoredVerificationCode::getPushCode).orElse(null),
storedChallenge.flatMap(StoredVerificationCode::getTwilioVerificationSid).orElse(null));
maybeStoredVerificationCode.map(StoredVerificationCode::pushCode).orElse(null),
maybeStoredVerificationCode.map(StoredVerificationCode::twilioVerificationSid).orElse(null),
maybeStoredVerificationCode.map(StoredVerificationCode::sessionId).orElse(null));
pendingAccounts.store(number, storedVerificationCode);
@@ -344,7 +394,11 @@ public class AccountController {
logger.warn("Error with Twilio Verify", throwable);
return;
}
if (enrolledInVerifyExperiment && maybeVerificationSid.isEmpty() && assessmentResult.isPresent()) {
final String countryCode = Util.getCountryCode(number);
final String region = Util.getRegion(number);
Metrics.counter(TWILIO_VERIFY_UNDELIVERED_COUNTER_NAME, Tags.of(
Tag.of(COUNTRY_CODE_TAG_NAME, countryCode),
Tag.of(REGION_TAG_NAME, region),
@@ -352,28 +406,61 @@ public class AccountController {
Tag.of(SCORE_TAG_NAME, assessmentResult.get().score())))
.increment();
}
maybeVerificationSid.ifPresent(twilioVerificationSid -> {
StoredVerificationCode storedVerificationCodeWithVerificationSid = new StoredVerificationCode(
storedVerificationCode.getCode(),
storedVerificationCode.getTimestamp(),
storedVerificationCode.getPushCode(),
twilioVerificationSid);
storedVerificationCode.code(),
storedVerificationCode.timestamp(),
storedVerificationCode.pushCode(),
twilioVerificationSid,
storedVerificationCode.sessionId());
pendingAccounts.store(number, storedVerificationCodeWithVerificationSid);
});
});
}
// TODO Remove this meter when external dependencies have been resolved
metricRegistry.meter(name(AccountController.class, "create", Util.getCountryCode(number))).mark();
private void sendVerificationCodeViaRegistrationService(final String number,
final Optional<StoredVerificationCode> maybeStoredVerificationCode,
final Optional<String> acceptLanguage,
final Optional<String> client,
final String transport) {
Metrics.counter(ACCOUNT_CREATE_COUNTER_NAME, Tags.of(
UserAgentTagUtil.getPlatformTag(userAgent),
Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)),
Tag.of(REGION_TAG_NAME, Util.getRegion(number)),
Tag.of(VERIFICATION_TRANSPORT_TAG_NAME, transport),
Tag.of(VERIFY_EXPERIMENT_TAG_NAME, String.valueOf(enrolledInVerifyExperiment))))
.increment();
final Phonenumber.PhoneNumber phoneNumber;
return Response.ok().build();
try {
phoneNumber = PhoneNumberUtil.getInstance().parse(number, null);
} catch (final NumberParseException e) {
throw new WebApplicationException(Response.status(422).build());
}
final MessageTransport messageTransport = switch (transport) {
case "sms" -> MessageTransport.SMS;
case "voice" -> MessageTransport.VOICE;
default -> throw new WebApplicationException(Response.status(422).build());
};
final ClientType clientType = client.map(clientTypeString -> {
if ("ios".equalsIgnoreCase(clientTypeString)) {
return ClientType.IOS;
} else if ("android-2021-03".equalsIgnoreCase(clientTypeString)) {
return ClientType.ANDROID_WITH_FCM;
} else if (StringUtils.startsWithIgnoreCase(clientTypeString, "android")) {
return ClientType.ANDROID_WITHOUT_FCM;
} else {
return ClientType.UNKNOWN;
}
}).orElse(ClientType.UNKNOWN);
final byte[] sessionId = registrationServiceClient.sendRegistrationCode(phoneNumber,
messageTransport, clientType, acceptLanguage.orElse(null), REGISTRATION_RPC_TIMEOUT).join();
final StoredVerificationCode storedVerificationCode = new StoredVerificationCode(null,
System.currentTimeMillis(),
maybeStoredVerificationCode.map(StoredVerificationCode::pushCode).orElse(null),
null,
sessionId);
pendingAccounts.store(number, storedVerificationCode);
}
@Timed
@@ -397,13 +484,20 @@ public class AccountController {
// Note that successful verification depends on being able to find a stored verification code for the given number.
// We check that numbers are normalized before we store verification codes, and so don't need to re-assert
// normalization here.
Optional<StoredVerificationCode> storedVerificationCode = pendingAccounts.getCodeForNumber(number);
final Optional<StoredVerificationCode> maybeStoredVerificationCode = pendingAccounts.getCodeForNumber(number);
if (storedVerificationCode.isEmpty() || !storedVerificationCode.get().isValid(verificationCode)) {
final boolean codeVerified = maybeStoredVerificationCode.map(storedVerificationCode ->
storedVerificationCode.sessionId() != null ?
registrationServiceClient.checkVerificationCode(storedVerificationCode.sessionId(),
verificationCode, REGISTRATION_RPC_TIMEOUT).join() :
storedVerificationCode.isValid(verificationCode))
.orElse(false);
if (!codeVerified) {
throw new WebApplicationException(Response.status(403).build());
}
storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid)
maybeStoredVerificationCode.map(StoredVerificationCode::twilioVerificationSid)
.ifPresent(
verificationSid -> smsSender.reportVerificationSucceeded(verificationSid, userAgent, "registration"));
@@ -427,8 +521,7 @@ public class AccountController {
Metrics.counter(ACCOUNT_VERIFY_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent),
Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)),
Tag.of(REGION_TAG_NAME, Util.getRegion(number)),
Tag.of(REGION_CODE_TAG_NAME, Util.getRegion(number)),
Tag.of(VERIFY_EXPERIMENT_TAG_NAME, String.valueOf(storedVerificationCode.get().getTwilioVerificationSid().isPresent()))))
Tag.of(REGION_CODE_TAG_NAME, Util.getRegion(number))))
.increment();
return new AccountIdentityResponse(account.getUuid(),
@@ -459,14 +552,13 @@ public class AccountController {
rateLimiters.getVerifyLimiter().validate(number);
final Optional<StoredVerificationCode> storedVerificationCode =
pendingAccounts.getCodeForNumber(number);
final Optional<StoredVerificationCode> storedVerificationCode = pendingAccounts.getCodeForNumber(number);
if (storedVerificationCode.isEmpty() || !storedVerificationCode.get().isValid(request.code())) {
throw new ForbiddenException();
}
storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid)
storedVerificationCode.map(StoredVerificationCode::twilioVerificationSid)
.ifPresent(
verificationSid -> smsSender.reportVerificationSucceeded(verificationSid, userAgent, "changeNumber"));
@@ -842,7 +934,7 @@ public class AccountController {
final Optional<StoredVerificationCode> storedVerificationCode) {
final String countryCode = Util.getCountryCode(number);
final String region = Util.getRegion(number);
Optional<String> storedPushChallenge = storedVerificationCode.map(StoredVerificationCode::getPushCode);
Optional<String> storedPushChallenge = storedVerificationCode.map(StoredVerificationCode::pushCode);
boolean match = Optionals.zipWith(pushChallenge, storedPushChallenge, String::equals).orElse(false);
Metrics.counter(PUSH_CHALLENGE_COUNTER_NAME,
COUNTRY_CODE_TAG_NAME, countryCode,

View File

@@ -132,11 +132,9 @@ public class DeviceController {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
VerificationCode verificationCode = generateVerificationCode();
StoredVerificationCode storedVerificationCode = new StoredVerificationCode(verificationCode.getVerificationCode(),
System.currentTimeMillis(),
null,
null);
VerificationCode verificationCode = generateVerificationCode();
StoredVerificationCode storedVerificationCode =
new StoredVerificationCode(verificationCode.getVerificationCode(), System.currentTimeMillis(), null, null, null);
pendingDevices.store(account.getNumber(), storedVerificationCode);

View File

@@ -0,0 +1,32 @@
package org.whispersystems.textsecuregcm.registration;
import io.grpc.CallCredentials;
import io.grpc.Metadata;
import java.util.concurrent.Executor;
class ApiKeyCallCredentials extends CallCredentials {
private final String apiKey;
private static final Metadata.Key<String> API_KEY_METADATA_KEY =
Metadata.Key.of("x-signal-api-key", Metadata.ASCII_STRING_MARSHALLER);
ApiKeyCallCredentials(final String apiKey) {
this.apiKey = apiKey;
}
@Override
public void applyRequestMetadata(final RequestInfo requestInfo,
final Executor appExecutor,
final MetadataApplier applier) {
final Metadata metadata = new Metadata();
metadata.put(API_KEY_METADATA_KEY, apiKey);
applier.apply(metadata);
}
@Override
public void thisUsesUnstableApi() {
}
}

View File

@@ -0,0 +1,13 @@
/*
* Copyright 2022 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.registration;
public enum ClientType {
IOS,
ANDROID_WITH_FCM,
ANDROID_WITHOUT_FCM,
UNKNOWN
}

View File

@@ -0,0 +1,14 @@
/*
* Copyright 2022 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.registration;
/**
* A message transport is a medium via which verification codes can be delivered to a destination phone.
*/
public enum MessageTransport {
SMS,
VOICE
}

View File

@@ -0,0 +1,138 @@
package org.whispersystems.textsecuregcm.registration;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
import com.google.protobuf.ByteString;
import io.dropwizard.lifecycle.Managed;
import io.grpc.ChannelCredentials;
import io.grpc.Deadline;
import io.grpc.Grpc;
import io.grpc.ManagedChannel;
import io.grpc.TlsChannelCredentials;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.signal.registration.rpc.CheckVerificationCodeRequest;
import org.signal.registration.rpc.CheckVerificationCodeResponse;
import org.signal.registration.rpc.RegistrationServiceGrpc;
import org.signal.registration.rpc.SendVerificationCodeRequest;
public class RegistrationServiceClient implements Managed {
private final ManagedChannel channel;
private final RegistrationServiceGrpc.RegistrationServiceFutureStub stub;
private final Executor callbackExecutor;
public RegistrationServiceClient(final String host,
final int port,
final String apiKey,
final String caCertificatePem,
final Executor callbackExecutor) throws IOException {
try (final ByteArrayInputStream certificateInputStream = new ByteArrayInputStream(caCertificatePem.getBytes(StandardCharsets.UTF_8))) {
final ChannelCredentials tlsChannelCredentials = TlsChannelCredentials.newBuilder()
.trustManager(certificateInputStream)
.build();
this.channel = Grpc.newChannelBuilderForAddress(host, port, tlsChannelCredentials).build();
}
this.stub = RegistrationServiceGrpc.newFutureStub(channel)
.withCallCredentials(new ApiKeyCallCredentials(apiKey));
this.callbackExecutor = callbackExecutor;
}
public CompletableFuture<byte[]> sendRegistrationCode(final Phonenumber.PhoneNumber phoneNumber,
final MessageTransport messageTransport,
final ClientType clientType,
@Nullable final String acceptLanguage,
final Duration timeout) {
final long e164 = Long.parseLong(
PhoneNumberUtil.getInstance().format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164).substring(1));
final SendVerificationCodeRequest.Builder requestBuilder = SendVerificationCodeRequest.newBuilder()
.setE164(e164)
.setTransport(getRpcMessageTransport(messageTransport))
.setClientType(getRpcClientType(clientType));
if (StringUtils.isNotBlank(acceptLanguage)) {
requestBuilder.setAcceptLanguage(acceptLanguage);
}
return toCompletableFuture(stub.withDeadline(toDeadline(timeout))
.sendVerificationCode(requestBuilder.build()))
.thenApply(response -> response.getSessionId().toByteArray());
}
public CompletableFuture<Boolean> checkVerificationCode(final byte[] sessionId,
final String verificationCode,
final Duration timeout) {
return toCompletableFuture(stub.withDeadline(toDeadline(timeout))
.checkVerificationCode(CheckVerificationCodeRequest.newBuilder()
.setSessionId(ByteString.copyFrom(sessionId))
.setVerificationCode(verificationCode)
.build()))
.thenApply(CheckVerificationCodeResponse::getVerified);
}
private static Deadline toDeadline(final Duration timeout) {
return Deadline.after(timeout.toMillis(), TimeUnit.MILLISECONDS);
}
private static org.signal.registration.rpc.ClientType getRpcClientType(final ClientType clientType) {
return switch (clientType) {
case IOS -> org.signal.registration.rpc.ClientType.CLIENT_TYPE_IOS;
case ANDROID_WITH_FCM -> org.signal.registration.rpc.ClientType.CLIENT_TYPE_ANDROID_WITH_FCM;
case ANDROID_WITHOUT_FCM -> org.signal.registration.rpc.ClientType.CLIENT_TYPE_ANDROID_WITHOUT_FCM;
case UNKNOWN -> org.signal.registration.rpc.ClientType.CLIENT_TYPE_UNSPECIFIED;
};
}
private static org.signal.registration.rpc.MessageTransport getRpcMessageTransport(final MessageTransport transport) {
return switch (transport) {
case SMS -> org.signal.registration.rpc.MessageTransport.MESSAGE_TRANSPORT_SMS;
case VOICE -> org.signal.registration.rpc.MessageTransport.MESSAGE_TRANSPORT_VOICE;
};
}
private <T> CompletableFuture<T> toCompletableFuture(final ListenableFuture<T> listenableFuture) {
final CompletableFuture<T> completableFuture = new CompletableFuture<>();
Futures.addCallback(listenableFuture, new FutureCallback<T>() {
@Override
public void onSuccess(@Nullable final T result) {
completableFuture.complete(result);
}
@Override
public void onFailure(final Throwable throwable) {
completableFuture.completeExceptionally(throwable);
}
}, callbackExecutor);
return completableFuture;
}
@Override
public void start() throws Exception {
}
@Override
public void stop() throws Exception {
if (channel != null) {
channel.shutdown();
}
}
}

View File

@@ -70,7 +70,7 @@ public class VerificationCodeStore {
}
private long getExpirationTimestamp(final StoredVerificationCode storedVerificationCode) {
return Instant.ofEpochMilli(storedVerificationCode.getTimestamp()).plus(StoredVerificationCode.EXPIRATION).getEpochSecond();
return Instant.ofEpochMilli(storedVerificationCode.timestamp()).plus(StoredVerificationCode.EXPIRATION).getEpochSecond();
}
public Optional<StoredVerificationCode> findForNumber(final String number) {