diff --git a/pom.xml b/pom.xml
index 96a5cead2..9f81ba92f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,6 +49,7 @@
2.9.0
2.0.32
1.1.13
+ 1.46.0
2.9.0
30.1.1-jre
2.13.3
@@ -94,6 +95,29 @@
pom
import
+
+ io.grpc
+ grpc-netty-shaded
+ ${grpc.version}
+ runtime
+
+
+ io.grpc
+ grpc-protobuf
+ ${grpc.version}
+
+
+ io.grpc
+ grpc-stub
+ ${grpc.version}
+
+
+
+ org.apache.tomcat
+ annotations-api
+ 6.0.53
+ provided
+
io.netty
netty-bom
@@ -368,13 +392,16 @@
protobuf-maven-plugin
0.6.1
- com.google.protobuf:protoc:3.18.0:exe:${os.detected.classifier}
- true
+ false
+ com.google.protobuf:protoc:3.21.1:exe:${os.detected.classifier}
+ grpc-java
+ io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
compile
+ compile-custom
test-compile
diff --git a/service/config/sample.yml b/service/config/sample.yml
index b851b80ec..84f6031ea 100644
--- a/service/config/sample.yml
+++ b/service/config/sample.yml
@@ -374,3 +374,29 @@ gift:
currencies:
# ISO 4217 currency codes and amounts in those currencies
xts: '2'
+
+registrationService:
+ host: registration.example.com
+ apiKey: EXAMPLE
+ registrationCaCertificate: | # Registration service TLS certificate trust root
+ -----BEGIN CERTIFICATE-----
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ AAAAAAAAAAAAAAAAAAAA
+ -----END CERTIFICATE-----
diff --git a/service/pom.xml b/service/pom.xml
index 9efba705a..305e1f53b 100644
--- a/service/pom.xml
+++ b/service/pom.xml
@@ -229,6 +229,26 @@
resilience4j-retry
+
+ io.grpc
+ grpc-netty-shaded
+ runtime
+
+
+ io.grpc
+ grpc-protobuf
+
+
+ io.grpc
+ grpc-stub
+
+
+
+ org.apache.tomcat
+ annotations-api
+ provided
+
+
io.micrometer
micrometer-core
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java
index e95accaa0..0a7bc4a32 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java
@@ -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;
+ }
}
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
index 9fb6b657e..bfe524c20 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
@@ -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 commonControllers = Lists.newArrayList(
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/StoredVerificationCode.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/StoredVerificationCode.java
index 6a0d6072b..e04c62ca3 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/auth/StoredVerificationCode.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/StoredVerificationCode.java
@@ -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 getTwilioVerificationSid() {
- return Optional.ofNullable(twilioVerificationSid);
- }
-
public boolean isValid(String theirCodeString) {
if (Util.isEmpty(code) || Util.isEmpty(theirCodeString)) {
return false;
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RegistrationServiceConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RegistrationServiceConfiguration.java
new file mode 100644
index 000000000..fdd38fb91
--- /dev/null
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/RegistrationServiceConfiguration.java
@@ -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;
+ }
+}
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java
index 5ce542ff8..96de80589 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java
@@ -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 dynamicConfigurationManager;
private final TurnTokenGenerator turnTokenGenerator;
private final Map 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 dynamicConfigurationManager,
TurnTokenGenerator turnTokenGenerator,
Map 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 storedChallenge = pendingAccounts.getCodeForNumber(number);
+ final String sourceHost = ForwardedIpUtil.getMostRecentProxy(forwardedFor).orElseThrow();
+ final Optional 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 maybeStoredVerificationCode,
+ final Optional acceptLanguage,
+ final String userAgent,
+ final Optional client,
+ final String transport,
+ final Optional 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 maybeStoredVerificationCode,
+ final Optional acceptLanguage,
+ final Optional 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 = pendingAccounts.getCodeForNumber(number);
+ final Optional 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 =
- pendingAccounts.getCodeForNumber(number);
+ final Optional 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) {
final String countryCode = Util.getCountryCode(number);
final String region = Util.getRegion(number);
- Optional storedPushChallenge = storedVerificationCode.map(StoredVerificationCode::getPushCode);
+ Optional 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,
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java
index d8fd1691e..3e81cb357 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java
@@ -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);
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/registration/ApiKeyCallCredentials.java b/service/src/main/java/org/whispersystems/textsecuregcm/registration/ApiKeyCallCredentials.java
new file mode 100644
index 000000000..f47ce01c4
--- /dev/null
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/registration/ApiKeyCallCredentials.java
@@ -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 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() {
+ }
+}
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/registration/ClientType.java b/service/src/main/java/org/whispersystems/textsecuregcm/registration/ClientType.java
new file mode 100644
index 000000000..7a5a9c546
--- /dev/null
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/registration/ClientType.java
@@ -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
+}
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/registration/MessageTransport.java b/service/src/main/java/org/whispersystems/textsecuregcm/registration/MessageTransport.java
new file mode 100644
index 000000000..f45f0f0e3
--- /dev/null
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/registration/MessageTransport.java
@@ -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
+}
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceClient.java b/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceClient.java
new file mode 100644
index 000000000..a75353596
--- /dev/null
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/registration/RegistrationServiceClient.java
@@ -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 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 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 CompletableFuture toCompletableFuture(final ListenableFuture listenableFuture) {
+ final CompletableFuture completableFuture = new CompletableFuture<>();
+
+ Futures.addCallback(listenableFuture, new FutureCallback() {
+ @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();
+ }
+ }
+}
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/VerificationCodeStore.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/VerificationCodeStore.java
index 78dc9a839..9f80d5f83 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/VerificationCodeStore.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/VerificationCodeStore.java
@@ -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 findForNumber(final String number) {
diff --git a/service/src/main/proto/RegistrationService.proto b/service/src/main/proto/RegistrationService.proto
new file mode 100644
index 000000000..ac49978f2
--- /dev/null
+++ b/service/src/main/proto/RegistrationService.proto
@@ -0,0 +1,84 @@
+syntax = "proto3";
+
+option java_multiple_files = true;
+
+package org.signal.registration.rpc;
+
+service RegistrationService {
+ /**
+ * Sends a verification code to a destination phone number and returns the
+ * ID of the newly-created registration session.
+ */
+ rpc send_verification_code (SendVerificationCodeRequest) returns (SendVerificationCodeResponse) {}
+
+ /**
+ * Checks a client-provided verification code for a given registration
+ * session.
+ */
+ rpc check_verification_code (CheckVerificationCodeRequest) returns (CheckVerificationCodeResponse) {}
+}
+
+message SendVerificationCodeRequest {
+ /**
+ * The phone number to which to send a verification code.
+ */
+ uint64 e164 = 1;
+
+ /**
+ * The message transport to use to send a verification code to the destination
+ * phone number.
+ */
+ MessageTransport transport = 2;
+
+ /**
+ * The value of the `Accept-Language` header provided by remote clients (if
+ * any).
+ */
+ string accept_language = 3;
+
+ /**
+ * The type of client requesting a verification code.
+ */
+ ClientType client_type = 4;
+}
+
+enum MessageTransport {
+ MESSAGE_TRANSPORT_UNSPECIFIED = 0;
+ MESSAGE_TRANSPORT_SMS = 1;
+ MESSAGE_TRANSPORT_VOICE = 2;
+}
+
+enum ClientType {
+ CLIENT_TYPE_UNSPECIFIED = 0;
+ CLIENT_TYPE_IOS = 1;
+ CLIENT_TYPE_ANDROID_WITH_FCM = 2;
+ CLIENT_TYPE_ANDROID_WITHOUT_FCM = 3;
+}
+
+message SendVerificationCodeResponse {
+ /**
+ * An opaque sequence of bytes that uniquely identifies the registration
+ * session associated with this registration attempt.
+ */
+ bytes session_id = 1;
+}
+
+message CheckVerificationCodeRequest {
+ /**
+ * The session ID returned when sending a verification code.
+ */
+ bytes session_id = 1;
+
+ /**
+ * The client-provided verification code.
+ */
+ string verification_code = 2;
+}
+
+message CheckVerificationCodeResponse {
+ /**
+ * The outcome of the verification attempt; true if the verification code
+ * matched the expected code or false otherwise.
+ */
+ bool verified = 1;
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/auth/StoredVerificationCodeTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/auth/StoredVerificationCodeTest.java
index 130f27bbc..7caae76ec 100644
--- a/service/src/test/java/org/whispersystems/textsecuregcm/auth/StoredVerificationCodeTest.java
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/auth/StoredVerificationCodeTest.java
@@ -22,9 +22,9 @@ class StoredVerificationCodeTest {
private static Stream isValid() {
return Stream.of(
- Arguments.of(new StoredVerificationCode("code", System.currentTimeMillis(), null, null), "code", true),
- Arguments.of(new StoredVerificationCode("code", System.currentTimeMillis(), null, null), "incorrect", false),
- Arguments.of(new StoredVerificationCode("", System.currentTimeMillis(), null, null), "", false)
+ Arguments.of(new StoredVerificationCode("code", System.currentTimeMillis(), null, null, null), "code", true),
+ Arguments.of(new StoredVerificationCode("code", System.currentTimeMillis(), null, null, null), "incorrect", false),
+ Arguments.of(new StoredVerificationCode("", System.currentTimeMillis(), null, null, null), "", false)
);
}
}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java
similarity index 94%
rename from service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java
rename to service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java
index 06e0544e7..4bf67ff9e 100644
--- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-package org.whispersystems.textsecuregcm.tests.controllers;
+package org.whispersystems.textsecuregcm.controllers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -26,12 +26,17 @@ import static org.mockito.Mockito.when;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.collect.ImmutableSet;
+import com.google.i18n.phonenumbers.NumberParseException;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import com.google.i18n.phonenumbers.Phonenumber;
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
import io.dropwizard.testing.junit5.ResourceExtension;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.time.Duration;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -66,8 +71,6 @@ import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicCaptchaConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
-import org.whispersystems.textsecuregcm.controllers.AccountController;
-import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.AccountIdentifierResponse;
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
@@ -83,6 +86,7 @@ import org.whispersystems.textsecuregcm.entities.ReserveUsernameResponse;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
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.mappers.ImpossiblePhoneNumberExceptionMapper;
@@ -92,6 +96,9 @@ import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper
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;
@@ -152,6 +159,7 @@ class AccountControllerTest {
private static RateLimiter usernameReserveLimiter = mock(RateLimiter.class);
private static RateLimiter usernameLookupLimiter = mock(RateLimiter.class);
private static SmsSender smsSender = mock(SmsSender.class);
+ private static RegistrationServiceClient registrationServiceClient = mock(RegistrationServiceClient.class);
private static TurnTokenGenerator turnTokenGenerator = mock(TurnTokenGenerator.class);
private static Account senderPinAccount = mock(Account.class);
private static Account senderRegLockAccount = mock(Account.class);
@@ -166,6 +174,8 @@ class AccountControllerTest {
private static TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager = mock(
TwilioVerifyExperimentEnrollmentManager.class);
+ private static ExperimentEnrollmentManager experimentEnrollmentManager = mock(ExperimentEnrollmentManager.class);
+
private byte[] registration_lock_key = new byte[32];
private static ExternalServiceCredentialGenerator storageCredentialGenerator = new ExternalServiceCredentialGenerator(new byte[32], new byte[32], false);
@@ -185,6 +195,7 @@ class AccountControllerTest {
abusiveHostRules,
rateLimiters,
smsSender,
+ registrationServiceClient,
dynamicConfigurationManager,
turnTokenGenerator,
Map.of(TEST_NUMBER, TEST_VERIFICATION_CODE),
@@ -192,7 +203,8 @@ class AccountControllerTest {
pushNotificationManager,
verifyExperimentEnrollmentManager,
changeNumberManager,
- storageCredentialGenerator))
+ storageCredentialGenerator,
+ experimentEnrollmentManager))
.build();
@@ -233,15 +245,15 @@ class AccountControllerTest {
when(senderTransfer.getUuid()).thenReturn(SENDER_TRANSFER_UUID);
when(senderTransfer.getNumber()).thenReturn(SENDER_TRANSFER);
- when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis(), "1234-push", null)));
+ when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis(), "1234-push", null, null)));
when(pendingAccountsManager.getCodeForNumber(SENDER_OLD)).thenReturn(Optional.empty());
- when(pendingAccountsManager.getCodeForNumber(SENDER_PIN)).thenReturn(Optional.of(new StoredVerificationCode("333333", System.currentTimeMillis(), null, null)));
- when(pendingAccountsManager.getCodeForNumber(SENDER_REG_LOCK)).thenReturn(Optional.of(new StoredVerificationCode("666666", System.currentTimeMillis(), null, null)));
- when(pendingAccountsManager.getCodeForNumber(SENDER_OVER_PIN)).thenReturn(Optional.of(new StoredVerificationCode("444444", System.currentTimeMillis(), null, null)));
- when(pendingAccountsManager.getCodeForNumber(SENDER_OVER_PREFIX)).thenReturn(Optional.of(new StoredVerificationCode("777777", System.currentTimeMillis(), "1234-push", null)));
- when(pendingAccountsManager.getCodeForNumber(SENDER_PREAUTH)).thenReturn(Optional.of(new StoredVerificationCode("555555", System.currentTimeMillis(), "validchallenge", null)));
- when(pendingAccountsManager.getCodeForNumber(SENDER_HAS_STORAGE)).thenReturn(Optional.of(new StoredVerificationCode("666666", System.currentTimeMillis(), null, null)));
- when(pendingAccountsManager.getCodeForNumber(SENDER_TRANSFER)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis(), null, null)));
+ when(pendingAccountsManager.getCodeForNumber(SENDER_PIN)).thenReturn(Optional.of(new StoredVerificationCode("333333", System.currentTimeMillis(), null, null, null)));
+ when(pendingAccountsManager.getCodeForNumber(SENDER_REG_LOCK)).thenReturn(Optional.of(new StoredVerificationCode("666666", System.currentTimeMillis(), null, null, null)));
+ when(pendingAccountsManager.getCodeForNumber(SENDER_OVER_PIN)).thenReturn(Optional.of(new StoredVerificationCode("444444", System.currentTimeMillis(), null, null, null)));
+ when(pendingAccountsManager.getCodeForNumber(SENDER_OVER_PREFIX)).thenReturn(Optional.of(new StoredVerificationCode("777777", System.currentTimeMillis(), "1234-push", null, null)));
+ when(pendingAccountsManager.getCodeForNumber(SENDER_PREAUTH)).thenReturn(Optional.of(new StoredVerificationCode("555555", System.currentTimeMillis(), "validchallenge", null, null)));
+ when(pendingAccountsManager.getCodeForNumber(SENDER_HAS_STORAGE)).thenReturn(Optional.of(new StoredVerificationCode("666666", System.currentTimeMillis(), null, null, null)));
+ when(pendingAccountsManager.getCodeForNumber(SENDER_TRANSFER)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis(), null, null, null)));
when(accountsManager.getByE164(eq(SENDER_PIN))).thenReturn(Optional.of(senderPinAccount));
when(accountsManager.getByE164(eq(SENDER_REG_LOCK))).thenReturn(Optional.of(senderRegLockAccount));
@@ -331,6 +343,7 @@ class AccountControllerTest {
usernameReserveLimiter,
usernameLookupLimiter,
smsSender,
+ registrationServiceClient,
turnTokenGenerator,
senderPinAccount,
senderRegLockAccount,
@@ -339,6 +352,7 @@ class AccountControllerTest {
recaptchaClient,
pushNotificationManager,
verifyExperimentEnrollmentManager,
+ experimentEnrollmentManager,
changeNumberManager);
clearInvocations(AuthHelper.DISABLED_DEVICE);
@@ -464,7 +478,7 @@ class AccountControllerTest {
@ParameterizedTest
@ValueSource(booleans = {false, true})
- void testSendCode(final boolean enrolledInVerifyExperiment) throws Exception {
+ void testSendCode(final boolean enrolledInVerifyExperiment) {
when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString()))
.thenReturn(enrolledInVerifyExperiment);
@@ -491,8 +505,8 @@ class AccountControllerTest {
verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(SENDER), eq(Optional.empty()), anyString(), eq(Collections.emptyList()));
verify(pendingAccountsManager, times(2)).store(eq(SENDER), storedVerificationCodeArgumentCaptor.capture());
- assertThat(storedVerificationCodeArgumentCaptor.getValue().getTwilioVerificationSid())
- .isEqualTo(Optional.of("VerificationSid"));
+ assertThat(storedVerificationCodeArgumentCaptor.getValue().twilioVerificationSid())
+ .isEqualTo("VerificationSid");
} else {
verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.empty()), anyString());
@@ -501,6 +515,37 @@ class AccountControllerTest {
verify(abusiveHostRules).isBlocked(eq(NICE_HOST));
}
+ @Test
+ void testSendCodeViaRegistrationService() throws NumberParseException {
+
+ final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
+
+ when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any()))
+ .thenReturn(CompletableFuture.completedFuture(sessionId));
+
+ when(experimentEnrollmentManager.isEnrolled(SENDER, AccountController.REGISTRATION_SERVICE_EXPERIMENT_NAME))
+ .thenReturn(true);
+
+ Response response =
+ resources.getJerseyTest()
+ .target(String.format("/v1/accounts/sms/code/%s", SENDER))
+ .queryParam("challenge", "1234-push")
+ .request()
+ .header("X-Forwarded-For", NICE_HOST)
+ .get();
+
+ assertThat(response.getStatus()).isEqualTo(200);
+
+ final Phonenumber.PhoneNumber expectedPhoneNumber = PhoneNumberUtil.getInstance().parse(SENDER, null);
+
+ verify(registrationServiceClient).sendRegistrationCode(expectedPhoneNumber, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT);
+ verify(abusiveHostRules).isBlocked(eq(NICE_HOST));
+ verify(pendingAccountsManager).store(eq(SENDER), argThat(
+ storedVerificationCode -> Arrays.equals(storedVerificationCode.sessionId(), sessionId) &&
+ "1234-push".equals(storedVerificationCode.pushCode())));
+
+ verifyNoInteractions(smsSender);
+ }
@Test
void testSendCodeImpossibleNumber() {
@@ -996,7 +1041,7 @@ class AccountControllerTest {
final String challenge = "challenge";
when(pendingAccountsManager.getCodeForNumber(RESTRICTED_NUMBER))
- .thenReturn(Optional.of(new StoredVerificationCode("123456", System.currentTimeMillis(), challenge, null)));
+ .thenReturn(Optional.of(new StoredVerificationCode("123456", System.currentTimeMillis(), challenge, null, null)));
Response response =
resources.getJerseyTest()
@@ -1033,7 +1078,7 @@ class AccountControllerTest {
final String challenge = "challenge";
when(pendingAccountsManager.getCodeForNumber(number))
- .thenReturn(Optional.of(new StoredVerificationCode("123456", System.currentTimeMillis(), challenge, null)));
+ .thenReturn(Optional.of(new StoredVerificationCode("123456", System.currentTimeMillis(), challenge, null, null)));
when(smsSender.deliverSmsVerificationWithTwilioVerify(any(), any(), any(), any()))
.thenReturn(CompletableFuture.completedFuture(Optional.empty()));
@@ -1073,7 +1118,7 @@ class AccountControllerTest {
}
final String challenge = "challenge";
- when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of(new StoredVerificationCode("123456", System.currentTimeMillis(), challenge, null)));
+ when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of(new StoredVerificationCode("123456", System.currentTimeMillis(), challenge, null, null)));
Response response =
resources.getJerseyTest()
@@ -1106,7 +1151,7 @@ class AccountControllerTest {
.get();
ArgumentCaptor captor = ArgumentCaptor.forClass(StoredVerificationCode.class);
verify(pendingAccountsManager).store(eq(TEST_NUMBER), captor.capture());
- assertThat(captor.getValue().getCode()).isEqualTo(Integer.toString(TEST_VERIFICATION_CODE));
+ assertThat(captor.getValue().code()).isEqualTo(Integer.toString(TEST_VERIFICATION_CODE));
assertThat(response.getStatus()).isEqualTo(200);
verifyNoInteractions(smsSender);
}
@@ -1116,7 +1161,7 @@ class AccountControllerTest {
void testVerifyCode(final boolean enrolledInVerifyExperiment) throws Exception {
if (enrolledInVerifyExperiment) {
when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(
- Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis(), "1234-push", "VerificationSid")));
+ Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis(), "1234-push", "VerificationSid", null)));
}
resources.getJerseyTest()
@@ -1130,6 +1175,31 @@ class AccountControllerTest {
if (enrolledInVerifyExperiment) {
verify(smsSender).reportVerificationSucceeded(eq("VerificationSid"), any(), eq("registration"));
}
+
+ verifyNoInteractions(registrationServiceClient);
+ }
+
+ @Test
+ void testVerifyCodeWithRegistrationService() throws Exception {
+ final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8);
+
+ when(pendingAccountsManager.getCodeForNumber(SENDER))
+ .thenReturn(Optional.of(
+ new StoredVerificationCode("1234", System.currentTimeMillis(), "1234-push", null, sessionId)));
+
+ when(registrationServiceClient.checkVerificationCode(sessionId, "1234", AccountController.REGISTRATION_RPC_TIMEOUT))
+ .thenReturn(CompletableFuture.completedFuture(true));
+
+ resources.getJerseyTest()
+ .target(String.format("/v1/accounts/code/%s", "1234"))
+ .request()
+ .header("Authorization", AuthHelper.getProvisioningAuthHeader(SENDER, "bar"))
+ .put(Entity.entity(new AccountAttributes(), MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class);
+
+ verify(accountsManager).create(eq(SENDER), eq("bar"), any(), any(), anyList());
+
+ verify(registrationServiceClient).checkVerificationCode(sessionId, "1234", AccountController.REGISTRATION_RPC_TIMEOUT);
+ verifyNoInteractions(smsSender);
}
@Test
@@ -1173,6 +1243,32 @@ class AccountControllerTest {
verifyNoMoreInteractions(accountsManager);
}
+ @Test
+ void testVerifyBadCodeWithRegistrationService() {
+ final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8);
+
+ when(pendingAccountsManager.getCodeForNumber(SENDER))
+ .thenReturn(Optional.of(
+ new StoredVerificationCode("1234", System.currentTimeMillis(), "1234-push", null, sessionId)));
+
+ when(registrationServiceClient.checkVerificationCode(any(), any(), any()))
+ .thenReturn(CompletableFuture.completedFuture(false));
+
+ Response response =
+ resources.getJerseyTest()
+ .target(String.format("/v1/accounts/code/%s", "1111"))
+ .request()
+ .header("Authorization", AuthHelper.getProvisioningAuthHeader(SENDER, "bar"))
+ .put(Entity.entity(new AccountAttributes(false, 3333, null, null, true, null),
+ MediaType.APPLICATION_JSON_TYPE));
+
+ assertThat(response.getStatus()).isEqualTo(403);
+
+ verify(registrationServiceClient).checkVerificationCode(sessionId, "1111", AccountController.REGISTRATION_RPC_TIMEOUT);
+ verifyNoInteractions(accountsManager);
+ verifyNoInteractions(smsSender);
+ }
+
@Test
void testVerifyRegistrationLock() throws Exception {
AccountIdentityResponse result =
@@ -1319,7 +1415,7 @@ class AccountControllerTest {
void testVerifyTestDeviceNumber() throws Exception {
when(pendingAccountsManager.getCodeForNumber(TEST_NUMBER)).thenReturn(Optional.of(
- new StoredVerificationCode(Integer.toString(TEST_VERIFICATION_CODE), System.currentTimeMillis(), "push", null)));
+ new StoredVerificationCode(Integer.toString(TEST_VERIFICATION_CODE), System.currentTimeMillis(), "push", null, null)));
final Response response = resources.getJerseyTest()
@@ -1339,7 +1435,7 @@ class AccountControllerTest {
final String code = "987654";
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
- new StoredVerificationCode(code, System.currentTimeMillis(), "push", null)));
+ new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null)));
final AccountIdentityResponse accountIdentityResponse =
resources.getJerseyTest()
@@ -1434,7 +1530,7 @@ class AccountControllerTest {
final String code = "987654";
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
- new StoredVerificationCode(code, System.currentTimeMillis(), "push", null)));
+ new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null)));
final Response response =
resources.getJerseyTest()
@@ -1454,7 +1550,7 @@ class AccountControllerTest {
final String code = "987654";
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
- new StoredVerificationCode(code, System.currentTimeMillis(), "push", null)));
+ new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null)));
final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class);
when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(false);
@@ -1484,7 +1580,7 @@ class AccountControllerTest {
final String code = "987654";
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
- new StoredVerificationCode(code, System.currentTimeMillis(), "push", null)));
+ new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null)));
final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class);
when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(true);
@@ -1515,7 +1611,7 @@ class AccountControllerTest {
final String reglock = "setec-astronomy";
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
- new StoredVerificationCode(code, System.currentTimeMillis(), "push", null)));
+ new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null)));
final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class);
when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(true);
@@ -1547,7 +1643,7 @@ class AccountControllerTest {
final String reglock = "setec-astronomy";
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
- new StoredVerificationCode(code, System.currentTimeMillis(), "push", null)));
+ new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null)));
final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class);
when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(true);
@@ -1593,7 +1689,7 @@ class AccountControllerTest {
when(AuthHelper.VALID_ACCOUNT.getDevice(3L)).thenReturn(Optional.of(device3));
when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of(
- new StoredVerificationCode(code, System.currentTimeMillis(), "push", null)));
+ new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null)));
var deviceMessages = List.of(
new IncomingMessage(1, 2, 2, "content2"),
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/VerificationCodeStoreTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/VerificationCodeStoreTest.java
index 2249d6e05..28b21731a 100644
--- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/VerificationCodeStoreTest.java
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/VerificationCodeStoreTest.java
@@ -9,8 +9,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
+import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
@@ -51,8 +53,8 @@ class VerificationCodeStoreTest {
void testStoreAndFind() {
assertEquals(Optional.empty(), verificationCodeStore.findForNumber(PHONE_NUMBER));
- final StoredVerificationCode originalCode = new StoredVerificationCode("1234", VALID_TIMESTAMP, "abcd", "0987");
- final StoredVerificationCode secondCode = new StoredVerificationCode("5678", VALID_TIMESTAMP, "efgh", "7890");
+ final StoredVerificationCode originalCode = new StoredVerificationCode("1234", VALID_TIMESTAMP, "abcd", "0987", "session".getBytes(StandardCharsets.UTF_8));
+ final StoredVerificationCode secondCode = new StoredVerificationCode("5678", VALID_TIMESTAMP, "efgh", "7890", "changed-session".getBytes(StandardCharsets.UTF_8));
verificationCodeStore.insert(PHONE_NUMBER, originalCode);
{
@@ -75,13 +77,13 @@ class VerificationCodeStoreTest {
void testRemove() {
assertEquals(Optional.empty(), verificationCodeStore.findForNumber(PHONE_NUMBER));
- verificationCodeStore.insert(PHONE_NUMBER, new StoredVerificationCode("1234", VALID_TIMESTAMP, "abcd", "0987"));
+ verificationCodeStore.insert(PHONE_NUMBER, new StoredVerificationCode("1234", VALID_TIMESTAMP, "abcd", "0987", "session".getBytes(StandardCharsets.UTF_8)));
assertTrue(verificationCodeStore.findForNumber(PHONE_NUMBER).isPresent());
verificationCodeStore.remove(PHONE_NUMBER);
assertFalse(verificationCodeStore.findForNumber(PHONE_NUMBER).isPresent());
- verificationCodeStore.insert(PHONE_NUMBER, new StoredVerificationCode("1234", EXPIRED_TIMESTAMP, "abcd", "0987"));
+ verificationCodeStore.insert(PHONE_NUMBER, new StoredVerificationCode("1234", EXPIRED_TIMESTAMP, "abcd", "0987", "session".getBytes(StandardCharsets.UTF_8)));
assertFalse(verificationCodeStore.findForNumber(PHONE_NUMBER).isPresent());
}
@@ -92,9 +94,10 @@ class VerificationCodeStoreTest {
return false;
}
- return Objects.equals(first.getCode(), second.getCode()) &&
- first.getTimestamp() == second.getTimestamp() &&
- Objects.equals(first.getPushCode(), second.getPushCode()) &&
- Objects.equals(first.getTwilioVerificationSid(), second.getTwilioVerificationSid());
+ return Objects.equals(first.code(), second.code()) &&
+ first.timestamp() == second.timestamp() &&
+ Objects.equals(first.pushCode(), second.pushCode()) &&
+ Objects.equals(first.twilioVerificationSid(), second.twilioVerificationSid()) &&
+ Arrays.equals(first.sessionId(), second.sessionId());
}
}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java
index 865d2ff6a..4e549f7d2 100644
--- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java
@@ -132,7 +132,7 @@ class DeviceControllerTest {
when(account.isGiftBadgesSupported()).thenReturn(true);
when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn(
- Optional.of(new StoredVerificationCode("5678901", System.currentTimeMillis(), null, null)));
+ Optional.of(new StoredVerificationCode("5678901", System.currentTimeMillis(), null, null, null)));
when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.empty());
when(accountsManager.getByE164(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(account));
when(accountsManager.getByE164(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(maxedAccount));