Add /v1/registration

This commit is contained in:
Chris Eager
2023-02-06 16:11:59 -06:00
committed by GitHub
parent 358a286523
commit a4a45de161
14 changed files with 872 additions and 126 deletions

View File

@@ -76,6 +76,7 @@ import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.auth.WebsocketRefreshApplicationEventListener;
import org.whispersystems.textsecuregcm.badges.ConfiguredProfileBadgeConverter;
@@ -101,6 +102,7 @@ import org.whispersystems.textsecuregcm.controllers.MessageController;
import org.whispersystems.textsecuregcm.controllers.PaymentsController;
import org.whispersystems.textsecuregcm.controllers.ProfileController;
import org.whispersystems.textsecuregcm.controllers.ProvisioningController;
import org.whispersystems.textsecuregcm.controllers.RegistrationController;
import org.whispersystems.textsecuregcm.controllers.RemoteConfigController;
import org.whispersystems.textsecuregcm.controllers.SecureBackupController;
import org.whispersystems.textsecuregcm.controllers.SecureStorageController;
@@ -518,6 +520,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
SubscriptionManager subscriptionManager = new SubscriptionManager(
config.getDynamoDbTables().getSubscriptions().getTableName(), dynamoDbAsyncClient);
final RegistrationLockVerificationManager registrationLockVerificationManager = new RegistrationLockVerificationManager(
accountsManager, clientPresenceManager, backupCredentialsGenerator, rateLimiters);
ReportedMessageMetricsListener reportedMessageMetricsListener = new ReportedMessageMetricsListener(accountsManager);
reportMessageManager.addListener(reportedMessageMetricsListener);
@@ -673,8 +678,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.jersey().register(
new AccountController(pendingAccountsManager, accountsManager, rateLimiters,
registrationServiceClient, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(),
captchaChecker, pushNotificationManager, changeNumberManager, registrationRecoveryPasswordsManager, backupCredentialsGenerator,
clientPresenceManager, clock));
captchaChecker, pushNotificationManager, changeNumberManager, registrationLockVerificationManager,
registrationRecoveryPasswordsManager, clock));
environment.jersey().register(new KeysController(rateLimiters, keys, accountsManager));
@@ -741,6 +746,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
profileBadgeConverter, config.getBadges(), cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner,
config.getCdnConfiguration().getBucket(), zkProfileOperations, batchIdentityCheckExecutor),
new ProvisioningController(rateLimiters, provisioningManager),
new RegistrationController(accountsManager, registrationServiceClient, registrationLockVerificationManager,
rateLimiters),
new RemoteConfigController(remoteConfigsManager, adminEventLogger,
config.getRemoteConfigConfiguration().getAuthorizedTokens(),
config.getRemoteConfigConfiguration().getGlobalConfig()),

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
import com.google.common.annotations.VisibleForTesting;
import javax.annotation.Nullable;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.util.Util;
public class RegistrationLockVerificationManager {
@VisibleForTesting
public static final int FAILURE_HTTP_STATUS = 423;
private static final String LOCKED_ACCOUNT_COUNTER_NAME =
name(RegistrationLockVerificationManager.class, "lockedAccount");
private static final String LOCK_REASON_TAG_NAME = "lockReason";
private static final String ALREADY_LOCKED_TAG_NAME = "alreadyLocked";
private final AccountsManager accounts;
private final ClientPresenceManager clientPresenceManager;
private final ExternalServiceCredentialsGenerator backupServiceCredentialGenerator;
private final RateLimiters rateLimiters;
public RegistrationLockVerificationManager(
final AccountsManager accounts, final ClientPresenceManager clientPresenceManager,
final ExternalServiceCredentialsGenerator backupServiceCredentialGenerator, final RateLimiters rateLimiters) {
this.accounts = accounts;
this.clientPresenceManager = clientPresenceManager;
this.backupServiceCredentialGenerator = backupServiceCredentialGenerator;
this.rateLimiters = rateLimiters;
}
/**
* Verifies the given registration lock credentials against the accounts current registration lock, if any
*
* @param account
* @param clientRegistrationLock
* @throws RateLimitExceededException
* @throws WebApplicationException
*/
public void verifyRegistrationLock(final Account account, @Nullable final String clientRegistrationLock)
throws RateLimitExceededException, WebApplicationException {
final StoredRegistrationLock existingRegistrationLock = account.getRegistrationLock();
final ExternalServiceCredentials existingBackupCredentials =
backupServiceCredentialGenerator.generateForUuid(account.getUuid());
if (!existingRegistrationLock.requiresClientRegistrationLock()) {
return;
}
if (!Util.isEmpty(clientRegistrationLock)) {
rateLimiters.getPinLimiter().validate(account.getNumber());
}
final String phoneNumber = account.getNumber();
if (!existingRegistrationLock.verify(clientRegistrationLock)) {
// At this point, the client verified ownership of the phone number but doesnt have the reglock PIN.
// Freezing the existing account credentials will definitively start the reglock timeout.
// Until the timeout, the current reglock can still be supplied,
// along with phone number verification, to restore access.
/*
boolean alreadyLocked = existingAccount.hasLockedCredentials();
Metrics.counter(LOCKED_ACCOUNT_COUNTER_NAME,
LOCK_REASON_TAG_NAME, "verifiedNumberFailedReglock",
ALREADY_LOCKED_TAG_NAME, Boolean.toString(alreadyLocked))
.increment();
final Account updatedAccount;
if (!alreadyLocked) {
updatedAccount = accounts.update(existingAccount, Account::lockAuthenticationCredentials);
} else {
updatedAccount = existingAccount;
}
List<Long> deviceIds = updatedAccount.getDevices().stream().map(Device::getId).toList();
clientPresenceManager.disconnectAllPresences(updatedAccount.getUuid(), deviceIds);
*/
throw new WebApplicationException(Response.status(FAILURE_HTTP_STATUS)
.entity(new RegistrationLockFailure(existingRegistrationLock.getTimeRemaining(),
existingRegistrationLock.needsFailureCredentials() ? existingBackupCredentials : null))
.build());
}
rateLimiters.getPinLimiter().clear(phoneNumber);
}
}

View File

@@ -23,15 +23,15 @@ public class RateLimitsConfiguration {
@JsonProperty
private RateLimitConfiguration smsVoicePrefix = new RateLimitConfiguration(1000, 1000);
@JsonProperty
private RateLimitConfiguration autoBlock = new RateLimitConfiguration(500, 500);
@JsonProperty
private RateLimitConfiguration verifyNumber = new RateLimitConfiguration(2, 2);
@JsonProperty
private RateLimitConfiguration verifyPin = new RateLimitConfiguration(10, 1 / (24.0 * 60.0));
@JsonProperty
private RateLimitConfiguration registration = new RateLimitConfiguration(2, 2);
@JsonProperty
private RateLimitConfiguration attachments = new RateLimitConfiguration(50, 50);
@@ -71,16 +71,9 @@ public class RateLimitsConfiguration {
@JsonProperty
private RateLimitConfiguration checkAccountExistence = new RateLimitConfiguration(1_000, 1_000 / 60.0);
@JsonProperty
private RateLimitConfiguration stories = new RateLimitConfiguration(10_000, 10_000 / (24.0 * 60.0));
@JsonProperty
private RateLimitConfiguration backupAuthCheck = new RateLimitConfiguration(100, 100 / (24.0 * 60.0));
public RateLimitConfiguration getAutoBlock() {
return autoBlock;
}
public RateLimitConfiguration getAllocateDevice() {
return allocateDevice;
}
@@ -129,6 +122,10 @@ public class RateLimitsConfiguration {
return verifyPin;
}
public RateLimitConfiguration getRegistration() {
return registration;
}
public RateLimitConfiguration getTurnAllocations() {
return turnAllocations;
}
@@ -161,10 +158,6 @@ public class RateLimitsConfiguration {
return checkAccountExistence;
}
public RateLimitConfiguration getStories() {
return stories;
}
public RateLimitConfiguration getBackupAuthCheck() {
return backupAuthCheck;
}

View File

@@ -32,7 +32,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletionException;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
@@ -61,10 +60,8 @@ import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.BasicAuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.ChangesDeviceEnabledState;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager;
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.auth.TurnToken;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
@@ -82,7 +79,6 @@ import org.whispersystems.textsecuregcm.entities.DeviceName;
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
import org.whispersystems.textsecuregcm.entities.RegistrationLock;
import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure;
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest;
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse;
import org.whispersystems.textsecuregcm.entities.StaleDevices;
@@ -91,7 +87,6 @@ import org.whispersystems.textsecuregcm.limits.RateLimitedByIp;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
import org.whispersystems.textsecuregcm.push.PushNotification;
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
import org.whispersystems.textsecuregcm.registration.ClientType;
@@ -137,7 +132,7 @@ public class AccountController {
.publishPercentiles(0.75, 0.95, 0.99, 0.999)
.distributionStatisticExpiry(Duration.ofHours(2))
.register(Metrics.globalRegistry);
private static final String LOCKED_ACCOUNT_COUNTER_NAME = name(AccountController.class, "lockedAccount");
private static final String CHALLENGE_PRESENT_TAG_NAME = "present";
private static final String CHALLENGE_MATCH_TAG_NAME = "matches";
private static final String COUNTRY_CODE_TAG_NAME = "countryCode";
@@ -150,25 +145,22 @@ public class AccountController {
private static final String REGION_CODE_TAG_NAME = "regionCode";
private static final String VERIFICATION_TRANSPORT_TAG_NAME = "transport";
private static final String SCORE_TAG_NAME = "score";
private static final String LOCK_REASON_TAG_NAME = "lockReason";
private static final String ALREADY_LOCKED_TAG_NAME = "alreadyLocked";
private final StoredVerificationCodeManager pendingAccounts;
private final AccountsManager accounts;
private final RateLimiters rateLimiters;
private final RegistrationServiceClient registrationServiceClient;
private final StoredVerificationCodeManager pendingAccounts;
private final AccountsManager accounts;
private final RateLimiters rateLimiters;
private final RegistrationServiceClient registrationServiceClient;
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
private final TurnTokenGenerator turnTokenGenerator;
private final Map<String, Integer> testDevices;
private final CaptchaChecker captchaChecker;
private final PushNotificationManager pushNotificationManager;
private final ExternalServiceCredentialsGenerator backupServiceCredentialsGenerator;
private final TurnTokenGenerator turnTokenGenerator;
private final Map<String, Integer> testDevices;
private final CaptchaChecker captchaChecker;
private final PushNotificationManager pushNotificationManager;
private final RegistrationLockVerificationManager registrationLockVerificationManager;
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
private final ChangeNumberManager changeNumberManager;
private final Clock clock;
private final ClientPresenceManager clientPresenceManager;
@VisibleForTesting
static final Duration REGISTRATION_RPC_TIMEOUT = Duration.ofSeconds(15);
@@ -184,9 +176,8 @@ public class AccountController {
CaptchaChecker captchaChecker,
PushNotificationManager pushNotificationManager,
ChangeNumberManager changeNumberManager,
RegistrationLockVerificationManager registrationLockVerificationManager,
RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
ExternalServiceCredentialsGenerator backupServiceCredentialsGenerator,
ClientPresenceManager clientPresenceManager,
Clock clock
) {
this.pendingAccounts = pendingAccounts;
@@ -198,34 +189,12 @@ public class AccountController {
this.turnTokenGenerator = turnTokenGenerator;
this.captchaChecker = captchaChecker;
this.pushNotificationManager = pushNotificationManager;
this.backupServiceCredentialsGenerator = backupServiceCredentialsGenerator;
this.registrationLockVerificationManager = registrationLockVerificationManager;
this.changeNumberManager = changeNumberManager;
this.clientPresenceManager = clientPresenceManager;
this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
this.clock = clock;
}
@VisibleForTesting
public AccountController(
StoredVerificationCodeManager pendingAccounts,
AccountsManager accounts,
RateLimiters rateLimiters,
RegistrationServiceClient registrationServiceClient,
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
TurnTokenGenerator turnTokenGenerator,
Map<String, Integer> testDevices,
CaptchaChecker captchaChecker,
PushNotificationManager pushNotificationManager,
ChangeNumberManager changeNumberManager,
RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
ExternalServiceCredentialsGenerator backupServiceCredentialsGenerator
) {
this(pendingAccounts, accounts, rateLimiters,
registrationServiceClient, dynamicConfigurationManager, turnTokenGenerator, testDevices, captchaChecker,
pushNotificationManager, changeNumberManager, registrationRecoveryPasswordsManager,
backupServiceCredentialsGenerator, null, Clock.systemUTC());
}
@Timed
@GET
@Path("/{type}/preauth/{token}/{number}")
@@ -424,7 +393,8 @@ public class AccountController {
});
if (existingAccount.isPresent()) {
verifyRegistrationLock(existingAccount.get(), accountAttributes.getRegistrationLock());
registrationLockVerificationManager.verifyRegistrationLock(existingAccount.get(),
accountAttributes.getRegistrationLock());
}
if (availableForTransfer.orElse(false) && existingAccount.map(Account::isTransferSupported).orElse(false)) {
@@ -488,7 +458,7 @@ public class AccountController {
final Optional<Account> existingAccount = accounts.getByE164(number);
if (existingAccount.isPresent()) {
verifyRegistrationLock(existingAccount.get(), request.registrationLock());
registrationLockVerificationManager.verifyRegistrationLock(existingAccount.get(), request.registrationLock());
}
rateLimiters.getVerifyLimiter().clear(number);
@@ -823,51 +793,6 @@ public class AccountController {
rateLimiter.validate(mostRecentProxy);
}
private void verifyRegistrationLock(final Account existingAccount, @Nullable final String clientRegistrationLock)
throws RateLimitExceededException, WebApplicationException {
final StoredRegistrationLock existingRegistrationLock = existingAccount.getRegistrationLock();
final ExternalServiceCredentials existingBackupCredentials =
backupServiceCredentialsGenerator.generateForUuid(existingAccount.getUuid());
if (existingRegistrationLock.requiresClientRegistrationLock()) {
if (!Util.isEmpty(clientRegistrationLock)) {
rateLimiters.getPinLimiter().validate(existingAccount.getNumber());
}
final String phoneNumber = existingAccount.getNumber();
if (!existingRegistrationLock.verify(clientRegistrationLock)) {
// At this point, the client verified ownership of the phone number but doesnt have the reglock PIN.
// Freezing the existing account credentials will definitively start the reglock timeout.
// Until the timeout, the current reglock can still be supplied,
// along with phone number verification, to restore access.
/* boolean alreadyLocked = existingAccount.hasLockedCredentials();
Metrics.counter(LOCKED_ACCOUNT_COUNTER_NAME,
LOCK_REASON_TAG_NAME, "verifiedNumberFailedReglock",
ALREADY_LOCKED_TAG_NAME, Boolean.toString(alreadyLocked))
.increment();
final Account updatedAccount;
if (!alreadyLocked) {
updatedAccount = accounts.update(existingAccount, Account::lockAuthenticationCredentials);
} else {
updatedAccount = existingAccount;
}
List<Long> deviceIds = updatedAccount.getDevices().stream().map(Device::getId).toList();
clientPresenceManager.disconnectAllPresences(updatedAccount.getUuid(), deviceIds); */
throw new WebApplicationException(Response.status(423)
.entity(new RegistrationLockFailure(existingRegistrationLock.getTimeRemaining(),
existingRegistrationLock.needsFailureCredentials() ? existingBackupCredentials : null))
.build());
}
rateLimiters.getPinLimiter().clear(phoneNumber);
}
}
@VisibleForTesting
static boolean pushChallengeMatches(
final String number,

View File

@@ -0,0 +1,166 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
import com.codahale.metrics.annotation.Timed;
import com.google.common.net.HttpHeaders;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import java.security.MessageDigest;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.Consumes;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.ServerErrorException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.BasicAuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager;
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
import org.whispersystems.textsecuregcm.entities.RegistrationRequest;
import org.whispersystems.textsecuregcm.entities.RegistrationSession;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.util.HeaderUtils;
import org.whispersystems.textsecuregcm.util.Util;
@Path("/v1/registration")
public class RegistrationController {
private static final Logger logger = LoggerFactory.getLogger(RegistrationController.class);
private static final DistributionSummary REREGISTRATION_IDLE_DAYS_DISTRIBUTION = DistributionSummary
.builder(name(RegistrationController.class, "reregistrationIdleDays"))
.publishPercentiles(0.75, 0.95, 0.99, 0.999)
.distributionStatisticExpiry(Duration.ofHours(2))
.register(Metrics.globalRegistry);
private static final String ACCOUNT_CREATED_COUNTER_NAME = name(RegistrationController.class, "accountCreated");
private static final String COUNTRY_CODE_TAG_NAME = "countryCode";
private static final String REGION_CODE_TAG_NAME = "regionCode";
private static final String VERIFICATION_TYPE_TAG_NAME = "verification";
private static final Duration REGISTRATION_RPC_TIMEOUT = Duration.ofSeconds(15);
private final AccountsManager accounts;
private final RegistrationServiceClient registrationServiceClient;
private final RegistrationLockVerificationManager registrationLockVerificationManager;
private final RateLimiters rateLimiters;
public RegistrationController(final AccountsManager accounts,
final RegistrationServiceClient registrationServiceClient,
final RegistrationLockVerificationManager registrationLockVerificationManager,
final RateLimiters rateLimiters) {
this.accounts = accounts;
this.registrationServiceClient = registrationServiceClient;
this.registrationLockVerificationManager = registrationLockVerificationManager;
this.rateLimiters = rateLimiters;
}
@Timed
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public AccountIdentityResponse register(
@HeaderParam(HttpHeaders.AUTHORIZATION) @NotNull final BasicAuthorizationHeader authorizationHeader,
@HeaderParam(HeaderUtils.X_SIGNAL_AGENT) final String signalAgent,
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
@NotNull @Valid final RegistrationRequest registrationRequest) throws RateLimitExceededException, InterruptedException {
rateLimiters.getRegistrationLimiter().validate(registrationRequest.sessionId());
final byte[] sessionId;
try {
sessionId = Base64.getDecoder().decode(registrationRequest.sessionId());
} catch (final IllegalArgumentException e) {
throw new ClientErrorException("Malformed session ID", HttpStatus.SC_UNPROCESSABLE_ENTITY);
}
final String number = authorizationHeader.getUsername();
final String password = authorizationHeader.getPassword();
final String verificationType = "phoneNumberVerification";
try {
final Optional<RegistrationSession> maybeSession = registrationServiceClient.getSession(sessionId,
REGISTRATION_RPC_TIMEOUT)
.get(REGISTRATION_RPC_TIMEOUT.plusSeconds(1).getSeconds(), TimeUnit.SECONDS);
final RegistrationSession session = maybeSession.orElseThrow(
() -> new NotAuthorizedException("session not verified"));
if (!MessageDigest.isEqual(number.getBytes(), session.number().getBytes())) {
throw new BadRequestException("number does not match session");
}
if (!session.verified()) {
throw new NotAuthorizedException("session not verified");
}
} catch (final CancellationException | ExecutionException | TimeoutException e) {
logger.error("Registration service failure", e);
throw new ServerErrorException(Response.Status.SERVICE_UNAVAILABLE);
}
final Optional<Account> existingAccount = accounts.getByE164(number);
existingAccount.ifPresent(account -> {
final Instant accountLastSeen = Instant.ofEpochMilli(account.getLastSeen());
final Duration timeSinceLastSeen = Duration.between(accountLastSeen, Instant.now());
REREGISTRATION_IDLE_DAYS_DISTRIBUTION.record(timeSinceLastSeen.toDays());
});
if (existingAccount.isPresent()) {
registrationLockVerificationManager.verifyRegistrationLock(existingAccount.get(),
registrationRequest.accountAttributes().getRegistrationLock());
}
if (!registrationRequest.skipDeviceTransfer() && existingAccount.map(Account::isTransferSupported).orElse(false)) {
// If a device transfer is possible, clients must explicitly opt out of a transfer (i.e. after prompting the user)
// before we'll let them create a new account "from scratch"
throw new WebApplicationException(Response.status(409, "device transfer available").build());
}
final Account account = accounts.create(number, password, signalAgent, registrationRequest.accountAttributes(),
existingAccount.map(Account::getBadges).orElseGet(ArrayList::new));
Metrics.counter(ACCOUNT_CREATED_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent),
Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)),
Tag.of(REGION_CODE_TAG_NAME, Util.getRegion(number)),
Tag.of(VERIFICATION_TYPE_TAG_NAME, verificationType)))
.increment();
return new AccountIdentityResponse(account.getUuid(),
account.getNumber(),
account.getPhoneNumberIdentifier(),
account.getUsernameHash().orElse(null),
existingAccount.map(Account::isStorageSupported).orElse(false));
}
}

View File

@@ -0,0 +1,16 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.entities;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
public record RegistrationRequest(@NotBlank String sessionId,
@NotNull @Valid AccountAttributes accountAttributes,
boolean skipDeviceTransfer) {
}

View File

@@ -0,0 +1,10 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.entities;
public record RegistrationSession(String number, boolean verified) {
}

View File

@@ -43,6 +43,7 @@ public class RateLimiters {
private final RateLimiter smsVoicePrefixLimiter;
private final RateLimiter verifyLimiter;
private final RateLimiter pinLimiter;
private final RateLimiter registrationLimiter;
private final RateLimiter attachmentLimiter;
private final RateLimiter preKeysLimiter;
private final RateLimiter messagesLimiter;
@@ -54,7 +55,6 @@ public class RateLimiters {
private final RateLimiter artPackLimiter;
private final RateLimiter usernameSetLimiter;
private final RateLimiter usernameReserveLimiter;
private final RateLimiter storiesLimiter;
private final Map<String, RateLimiter> rateLimiterByHandle;
@@ -66,6 +66,7 @@ public class RateLimiters {
this.smsVoicePrefixLimiter = fromConfig("smsVoicePrefix", config.getSmsVoicePrefix(), cacheCluster);
this.verifyLimiter = fromConfig("verify", config.getVerifyNumber(), cacheCluster);
this.pinLimiter = fromConfig("pin", config.getVerifyPin(), cacheCluster);
this.registrationLimiter = fromConfig("registration", config.getRegistration(), cacheCluster);
this.attachmentLimiter = fromConfig("attachmentCreate", config.getAttachments(), cacheCluster);
this.preKeysLimiter = fromConfig("prekeys", config.getPreKeys(), cacheCluster);
this.messagesLimiter = fromConfig("messages", config.getMessages(), cacheCluster);
@@ -77,7 +78,6 @@ public class RateLimiters {
this.artPackLimiter = fromConfig("artPack", config.getArtPack(), cacheCluster);
this.usernameSetLimiter = fromConfig("usernameSet", config.getUsernameSet(), cacheCluster);
this.usernameReserveLimiter = fromConfig("usernameReserve", config.getUsernameReserve(), cacheCluster);
this.storiesLimiter = fromConfig("stories", config.getStories(), cacheCluster);
this.rateLimiterByHandle = Stream.of(
fromConfig(Handle.BACKUP_AUTH_CHECK.id(), config.getBackupAuthCheck(), cacheCluster),
@@ -138,6 +138,10 @@ public class RateLimiters {
return pinLimiter;
}
public RateLimiter getRegistrationLimiter() {
return registrationLimiter;
}
public RateLimiter getTurnLimiter() {
return turnLimiter;
}
@@ -170,10 +174,6 @@ public class RateLimiters {
return byHandle(Handle.CHECK_ACCOUNT_EXISTENCE).orElseThrow();
}
public RateLimiter getStoriesLimiter() {
return storiesLimiter;
}
private static RateLimiter fromConfig(
final String name,
final RateLimitsConfiguration.RateLimitConfiguration cfg,

View File

@@ -3,6 +3,7 @@ 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.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
import com.google.protobuf.ByteString;
@@ -16,7 +17,7 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
@@ -24,11 +25,12 @@ 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.CreateRegistrationSessionRequest;
import org.signal.registration.rpc.GetRegistrationSessionMetadataRequest;
import org.signal.registration.rpc.RegistrationServiceGrpc;
import org.signal.registration.rpc.SendVerificationCodeRequest;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.entities.RegistrationSession;
public class RegistrationServiceClient implements Managed {
@@ -36,6 +38,24 @@ public class RegistrationServiceClient implements Managed {
private final RegistrationServiceGrpc.RegistrationServiceFutureStub stub;
private final Executor callbackExecutor;
/**
* @param from an e164 in a {@code long} representation e.g. {@code 18005550123}
* @return the e164 in a {@code String} representation (e.g. {@code "+18005550123"})
* @throws IllegalArgumentException if the number cannot be parsed to a string
*/
static String convertNumeralE164ToString(long from) {
try {
final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance()
.parse("+" + from, null);
return PhoneNumberUtil.getInstance()
.format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
} catch (final NumberParseException e) {
throw new IllegalArgumentException("could not parse to phone number", e);
}
}
public RegistrationServiceClient(final String host,
final int port,
final String apiKey,
@@ -116,9 +136,9 @@ public class RegistrationServiceClient implements Managed {
return toCompletableFuture(stub.withDeadline(toDeadline(timeout))
.checkVerificationCode(CheckVerificationCodeRequest.newBuilder()
.setSessionId(ByteString.copyFrom(sessionId))
.setVerificationCode(verificationCode)
.build()))
.setSessionId(ByteString.copyFrom(sessionId))
.setVerificationCode(verificationCode)
.build()))
.thenApply(response -> {
if (response.hasError()) {
switch (response.getError().getErrorType()) {
@@ -133,6 +153,26 @@ public class RegistrationServiceClient implements Managed {
});
}
public CompletableFuture<Optional<RegistrationSession>> getSession(final byte[] sessionId,
final Duration timeout) {
return toCompletableFuture(stub.withDeadline(toDeadline(timeout)).getSessionMetadata(
GetRegistrationSessionMetadataRequest.newBuilder()
.setSessionId(ByteString.copyFrom(sessionId)).build()))
.thenApply(response -> {
if (response.hasError()) {
switch (response.getError().getErrorType()) {
case GET_REGISTRATION_SESSION_METADATA_ERROR_TYPE_NOT_FOUND -> {
return Optional.empty();
}
default -> throw new RuntimeException("Failed to get session: " + response.getError().getErrorType());
}
}
final String number = convertNumeralE164ToString(response.getSessionMetadata().getE164());
return Optional.of(new RegistrationSession(number, response.getSessionMetadata().getVerified()));
});
}
private static Deadline toDeadline(final Duration timeout) {
return Deadline.after(timeout.toMillis(), TimeUnit.MILLISECONDS);
}