mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-19 19:48:04 +01:00
Add experiment to test standalone registration service
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 isn’t 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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user