Use UUIDs as rate limiter keys.

This commit is contained in:
Jon Chambers 2021-06-30 16:31:21 -04:00 committed by Jon Chambers
parent becf6afbdd
commit a680639718
20 changed files with 98 additions and 89 deletions

View File

@ -567,7 +567,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
smsSender, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(), smsSender, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(),
transitionalRecaptchaClient, gcmSender, apnSender, backupCredentialsGenerator, transitionalRecaptchaClient, gcmSender, apnSender, backupCredentialsGenerator,
verifyExperimentEnrollmentManager)); verifyExperimentEnrollmentManager));
environment.jersey().register(new KeysController(rateLimiters, keysDynamoDb, accountsManager, preKeyRateLimiter, dynamicConfigurationManager, rateLimitChallengeManager)); environment.jersey().register(new KeysController(rateLimiters, keysDynamoDb, accountsManager, preKeyRateLimiter, rateLimitChallengeManager));
final List<Object> commonControllers = List.of( final List<Object> commonControllers = List.of(
new AttachmentControllerV1(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getBucket()), new AttachmentControllerV1(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getBucket()),

View File

@ -207,17 +207,17 @@ public class AccountController {
throw new WebApplicationException(Response.status(400).build()); throw new WebApplicationException(Response.status(400).build());
} }
String requester = ForwardedIpUtil.getMostRecentProxy(forwardedFor).orElseThrow(); String sourceHost = ForwardedIpUtil.getMostRecentProxy(forwardedFor).orElseThrow();
Optional<StoredVerificationCode> storedChallenge = pendingAccounts.getCodeForNumber(number); Optional<StoredVerificationCode> storedChallenge = pendingAccounts.getCodeForNumber(number);
CaptchaRequirement requirement = requiresCaptcha(number, transport, forwardedFor, requester, captcha, storedChallenge, pushChallenge); CaptchaRequirement requirement = requiresCaptcha(number, transport, forwardedFor, sourceHost, captcha, storedChallenge, pushChallenge);
if (requirement.isCaptchaRequired()) { if (requirement.isCaptchaRequired()) {
captchaRequiredMeter.mark(); captchaRequiredMeter.mark();
if (requirement.isAutoBlock() && shouldAutoBlock(requester)) { if (requirement.isAutoBlock() && shouldAutoBlock(sourceHost)) {
logger.info("Auto-block: " + requester); logger.info("Auto-block: {}", sourceHost);
abusiveHostRules.setBlockedHost(requester, "Auto-Block"); abusiveHostRules.setBlockedHost(sourceHost, "Auto-Block");
} }
return Response.status(402).build(); return Response.status(402).build();
@ -405,7 +405,7 @@ public class AccountController {
@Path("/turn/") @Path("/turn/")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public TurnToken getTurnToken(@Auth Account account) throws RateLimitExceededException { public TurnToken getTurnToken(@Auth Account account) throws RateLimitExceededException {
rateLimiters.getTurnLimiter().validate(account.getNumber()); rateLimiters.getTurnLimiter().validate(account.getUuid());
return turnTokenGenerator.generate(); return turnTokenGenerator.generate();
} }
@ -568,7 +568,7 @@ public class AccountController {
@Path("/username/{username}") @Path("/username/{username}")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Response setUsername(@Auth Account account, @PathParam("username") String username) throws RateLimitExceededException { public Response setUsername(@Auth Account account, @PathParam("username") String username) throws RateLimitExceededException {
rateLimiters.getUsernameSetLimiter().validate(account.getUuid().toString()); rateLimiters.getUsernameSetLimiter().validate(account.getUuid());
if (username == null || username.isEmpty()) { if (username == null || username.isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST).build(); return Response.status(Response.Status.BAD_REQUEST).build();
@ -588,14 +588,14 @@ public class AccountController {
} }
private CaptchaRequirement requiresCaptcha(String number, String transport, String forwardedFor, private CaptchaRequirement requiresCaptcha(String number, String transport, String forwardedFor,
String requester, String sourceHost,
Optional<String> captchaToken, Optional<String> captchaToken,
Optional<StoredVerificationCode> storedVerificationCode, Optional<StoredVerificationCode> storedVerificationCode,
Optional<String> pushChallenge) Optional<String> pushChallenge)
{ {
if (captchaToken.isPresent()) { if (captchaToken.isPresent()) {
boolean validToken = recaptchaClient.verify(captchaToken.get(), requester); boolean validToken = recaptchaClient.verify(captchaToken.get(), sourceHost);
if (validToken) { if (validToken) {
captchaSuccessMeter.mark(); captchaSuccessMeter.mark();
@ -633,18 +633,18 @@ public class AccountController {
} }
} }
List<AbusiveHostRule> abuseRules = abusiveHostRules.getAbusiveHostRulesFor(requester); List<AbusiveHostRule> abuseRules = abusiveHostRules.getAbusiveHostRulesFor(sourceHost);
for (AbusiveHostRule abuseRule : abuseRules) { for (AbusiveHostRule abuseRule : abuseRules) {
if (abuseRule.isBlocked()) { if (abuseRule.isBlocked()) {
logger.info("Blocked host: " + transport + ", " + number + ", " + requester + " (" + forwardedFor + ")"); logger.info("Blocked host: {}, {}, {} ({})", transport, number, sourceHost, forwardedFor);
blockedHostMeter.mark(); blockedHostMeter.mark();
return new CaptchaRequirement(true, false); return new CaptchaRequirement(true, false);
} }
if (!abuseRule.getRegions().isEmpty()) { if (!abuseRule.getRegions().isEmpty()) {
if (abuseRule.getRegions().stream().noneMatch(number::startsWith)) { if (abuseRule.getRegions().stream().noneMatch(number::startsWith)) {
logger.info("Restricted host: " + transport + ", " + number + ", " + requester + " (" + forwardedFor + ")"); logger.info("Restricted host: {}, {}, {} ({})", transport, number, sourceHost, forwardedFor);
filteredHostMeter.mark(); filteredHostMeter.mark();
return new CaptchaRequirement(true, false); return new CaptchaRequirement(true, false);
} }
@ -652,9 +652,9 @@ public class AccountController {
} }
try { try {
rateLimiters.getSmsVoiceIpLimiter().validate(requester); rateLimiters.getSmsVoiceIpLimiter().validate(sourceHost);
} catch (RateLimitExceededException e) { } catch (RateLimitExceededException e) {
logger.info("Rate limited exceeded: " + transport + ", " + number + ", " + requester + " (" + forwardedFor + ")"); logger.info("Rate limit exceeded: {}, {}, {} ({})", transport, number, sourceHost, forwardedFor);
rateLimitedHostMeter.mark(); rateLimitedHostMeter.mark();
return new CaptchaRequirement(true, true); return new CaptchaRequirement(true, true);
} }
@ -662,7 +662,7 @@ public class AccountController {
try { try {
rateLimiters.getSmsVoicePrefixLimiter().validate(Util.getNumberPrefix(number)); rateLimiters.getSmsVoicePrefixLimiter().validate(Util.getNumberPrefix(number));
} catch (RateLimitExceededException e) { } catch (RateLimitExceededException e) {
logger.info("Prefix rate limit exceeded: " + transport + ", " + number + ", (" + forwardedFor + ")"); logger.info("Prefix rate limit exceeded: {}, {}, {} ({})", transport, number, sourceHost, forwardedFor);
rateLimitedPrefixMeter.mark(); rateLimitedPrefixMeter.mark();
return new CaptchaRequirement(true, true); return new CaptchaRequirement(true, true);
} }
@ -682,9 +682,9 @@ public class AccountController {
accounts.delete(account, AccountsManager.DeletionReason.USER_REQUEST); accounts.delete(account, AccountsManager.DeletionReason.USER_REQUEST);
} }
private boolean shouldAutoBlock(String requester) { private boolean shouldAutoBlock(String sourceHost) {
try { try {
rateLimiters.getAutoBlockLimiter().validate(requester); rateLimiters.getAutoBlockLimiter().validate(sourceHost);
} catch (RateLimitExceededException e) { } catch (RateLimitExceededException e) {
return true; return true;
} }

View File

@ -49,7 +49,7 @@ public class AttachmentControllerV1 extends AttachmentControllerBase {
throws RateLimitExceededException throws RateLimitExceededException
{ {
if (account.isRateLimited()) { if (account.isRateLimited()) {
rateLimiters.getAttachmentLimiter().validate(account.getNumber()); rateLimiters.getAttachmentLimiter().validate(account.getUuid());
} }
long attachmentId = generateAttachmentId(); long attachmentId = generateAttachmentId();

View File

@ -41,7 +41,7 @@ public class AttachmentControllerV2 extends AttachmentControllerBase {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Path("/form/upload") @Path("/form/upload")
public AttachmentDescriptorV2 getAttachmentUploadForm(@Auth Account account) throws RateLimitExceededException { public AttachmentDescriptorV2 getAttachmentUploadForm(@Auth Account account) throws RateLimitExceededException {
rateLimiter.validate(account.getNumber()); rateLimiter.validate(account.getUuid());
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC); ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
long attachmentId = generateAttachmentId(); long attachmentId = generateAttachmentId();

View File

@ -58,7 +58,7 @@ public class AttachmentControllerV3 extends AttachmentControllerBase {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Path("/form/upload") @Path("/form/upload")
public AttachmentDescriptorV3 getAttachmentUploadForm(@Auth Account account) throws RateLimitExceededException { public AttachmentDescriptorV3 getAttachmentUploadForm(@Auth Account account) throws RateLimitExceededException {
rateLimiter.validate(account.getNumber()); rateLimiter.validate(account.getUuid());
final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC); final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
final String key = generateAttachmentKey(); final String key = generateAttachmentKey();

View File

@ -112,7 +112,7 @@ public class DeviceController {
public VerificationCode createDeviceToken(@Auth Account account) public VerificationCode createDeviceToken(@Auth Account account)
throws RateLimitExceededException, DeviceLimitExceededException throws RateLimitExceededException, DeviceLimitExceededException
{ {
rateLimiters.getAllocateDeviceLimiter().validate(account.getNumber()); rateLimiters.getAllocateDeviceLimiter().validate(account.getUuid());
int maxDeviceLimit = MAX_DEVICES; int maxDeviceLimit = MAX_DEVICES;

View File

@ -40,11 +40,9 @@ import org.whispersystems.textsecuregcm.limits.PreKeyRateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeException; import org.whispersystems.textsecuregcm.limits.RateLimitChallengeException;
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager; import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb; import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
@ -57,7 +55,6 @@ public class KeysController {
private final AccountsManager accounts; private final AccountsManager accounts;
private final PreKeyRateLimiter preKeyRateLimiter; private final PreKeyRateLimiter preKeyRateLimiter;
private final DynamicConfigurationManager dynamicConfigurationManager;
private final RateLimitChallengeManager rateLimitChallengeManager; private final RateLimitChallengeManager rateLimitChallengeManager;
private static final String PREKEY_REQUEST_COUNTER_NAME = name(KeysController.class, "preKeyGet"); private static final String PREKEY_REQUEST_COUNTER_NAME = name(KeysController.class, "preKeyGet");
@ -69,14 +66,11 @@ public class KeysController {
public KeysController(RateLimiters rateLimiters, KeysDynamoDb keysDynamoDb, AccountsManager accounts, public KeysController(RateLimiters rateLimiters, KeysDynamoDb keysDynamoDb, AccountsManager accounts,
PreKeyRateLimiter preKeyRateLimiter, PreKeyRateLimiter preKeyRateLimiter,
DynamicConfigurationManager dynamicConfigurationManager,
RateLimitChallengeManager rateLimitChallengeManager) { RateLimitChallengeManager rateLimitChallengeManager) {
this.rateLimiters = rateLimiters; this.rateLimiters = rateLimiters;
this.keysDynamoDb = keysDynamoDb; this.keysDynamoDb = keysDynamoDb;
this.accounts = accounts; this.accounts = accounts;
this.preKeyRateLimiter = preKeyRateLimiter; this.preKeyRateLimiter = preKeyRateLimiter;
this.dynamicConfigurationManager = dynamicConfigurationManager;
this.rateLimitChallengeManager = rateLimitChallengeManager; this.rateLimitChallengeManager = rateLimitChallengeManager;
} }
@ -152,7 +146,7 @@ public class KeysController {
} }
if (account.isPresent()) { if (account.isPresent()) {
rateLimiters.getPreKeysLimiter().validate(account.get().getNumber() + "." + account.get().getAuthenticatedDevice().get().getId() + "__" + target.get().getNumber() + "." + deviceId); rateLimiters.getPreKeysLimiter().validate(account.get().getUuid() + "." + account.get().getAuthenticatedDevice().get().getId() + "__" + target.get().getUuid() + "." + deviceId);
try { try {
preKeyRateLimiter.validate(account.get()); preKeyRateLimiter.validate(account.get());

View File

@ -257,7 +257,7 @@ public class MessageController {
assert(destination.isPresent()); assert(destination.isPresent());
if (source.isPresent() && !source.get().isFor(destinationName)) { if (source.isPresent() && !source.get().isFor(destinationName)) {
rateLimiters.getMessagesLimiter().validate(source.get().getNumber() + "__" + destination.get().getUuid()); rateLimiters.getMessagesLimiter().validate(source.get().getUuid(), destination.get().getUuid());
final String senderCountryCode = Util.getCountryCode(source.get().getNumber()); final String senderCountryCode = Util.getCountryCode(source.get().getNumber());

View File

@ -210,7 +210,7 @@ public class ProfileController {
} }
if (requestAccount.isPresent()) { if (requestAccount.isPresent()) {
rateLimiters.getProfileLimiter().validate(requestAccount.get().getNumber()); rateLimiters.getProfileLimiter().validate(requestAccount.get().getUuid());
} }
Optional<Account> accountProfile = accountsManager.get(uuid); Optional<Account> accountProfile = accountsManager.get(uuid);
@ -260,7 +260,7 @@ public class ProfileController {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Path("/username/{username}") @Path("/username/{username}")
public Profile getProfileByUsername(@Auth Account account, @PathParam("username") String username) throws RateLimitExceededException { public Profile getProfileByUsername(@Auth Account account, @PathParam("username") String username) throws RateLimitExceededException {
rateLimiters.getUsernameLookupLimiter().validate(account.getUuid().toString()); rateLimiters.getUsernameLookupLimiter().validate(account.getUuid());
username = username.toLowerCase(); username = username.toLowerCase();
@ -341,7 +341,7 @@ public class ProfileController {
} }
if (requestAccount.isPresent()) { if (requestAccount.isPresent()) {
rateLimiters.getProfileLimiter().validate(requestAccount.get().getNumber()); rateLimiters.getProfileLimiter().validate(requestAccount.get().getUuid());
} }
Optional<Account> accountProfile = accountsManager.get(identifier); Optional<Account> accountProfile = accountsManager.get(identifier);

View File

@ -6,13 +6,8 @@
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed; import com.codahale.metrics.annotation.Timed;
import org.whispersystems.textsecuregcm.entities.ProvisioningMessage; import io.dropwizard.auth.Auth;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import java.util.Base64;
import org.whispersystems.textsecuregcm.push.ProvisioningManager;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException;
import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress;
import javax.validation.Valid; import javax.validation.Valid;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.PUT; import javax.ws.rs.PUT;
@ -22,10 +17,11 @@ import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException; import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException; import org.whispersystems.textsecuregcm.entities.ProvisioningMessage;
import java.util.Base64; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.push.ProvisioningManager;
import io.dropwizard.auth.Auth; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress;
@Path("/v1/provisioning") @Path("/v1/provisioning")
public class ProvisioningController { public class ProvisioningController {
@ -46,9 +42,9 @@ public class ProvisioningController {
public void sendProvisioningMessage(@Auth Account source, public void sendProvisioningMessage(@Auth Account source,
@PathParam("destination") String destinationName, @PathParam("destination") String destinationName,
@Valid ProvisioningMessage message) @Valid ProvisioningMessage message)
throws RateLimitExceededException, InvalidWebsocketAddressException, IOException throws RateLimitExceededException {
{
rateLimiters.getMessagesLimiter().validate(source.getNumber()); rateLimiters.getMessagesLimiter().validate(source.getUuid());
if (!provisioningManager.sendProvisioningMessage(new ProvisioningAddress(destinationName, 0), if (!provisioningManager.sendProvisioningMessage(new ProvisioningAddress(destinationName, 0),
Base64.getDecoder().decode(message.getBody()))) Base64.getDecoder().decode(message.getBody())))

View File

@ -49,7 +49,7 @@ public class StickerController {
@PathParam("count") @Min(1) @Max(201) int stickerCount) @PathParam("count") @Min(1) @Max(201) int stickerCount)
throws RateLimitExceededException throws RateLimitExceededException
{ {
rateLimiters.getStickerPackLimiter().validate(account.getNumber()); rateLimiters.getStickerPackLimiter().validate(account.getUuid());
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC); ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
String packId = generatePackId(); String packId = generatePackId();

View File

@ -49,7 +49,7 @@ public class PreKeyRateLimiter {
public void validate(final Account account) throws RateLimitExceededException { public void validate(final Account account) throws RateLimitExceededException {
try { try {
rateLimiters.getDailyPreKeysLimiter().validate(account.getNumber()); rateLimiters.getDailyPreKeysLimiter().validate(account.getUuid());
} catch (final RateLimitExceededException e) { } catch (final RateLimitExceededException e) {
final boolean enforceLimit = dynamicConfigurationManager.getConfiguration() final boolean enforceLimit = dynamicConfigurationManager.getConfiguration()
@ -70,7 +70,7 @@ public class PreKeyRateLimiter {
public void handleRateLimitReset(final Account account) { public void handleRateLimitReset(final Account account) {
rateLimiters.getDailyPreKeysLimiter().clear(account.getNumber()); rateLimiters.getDailyPreKeysLimiter().clear(account.getUuid());
Metrics.counter(RATE_LIMIT_RESET_COUNTER_NAME, "countryCode", Util.getCountryCode(account.getNumber())) Metrics.counter(RATE_LIMIT_RESET_COUNTER_NAME, "countryCode", Util.getCountryCode(account.getNumber()))
.increment(); .increment();

View File

@ -54,12 +54,12 @@ public class RateLimitChallengeManager {
} }
public void answerPushChallenge(final Account account, final String challenge) throws RateLimitExceededException { public void answerPushChallenge(final Account account, final String challenge) throws RateLimitExceededException {
rateLimiters.getPushChallengeAttemptLimiter().validate(account.getNumber()); rateLimiters.getPushChallengeAttemptLimiter().validate(account.getUuid());
final boolean challengeSuccess = pushChallengeManager.answerChallenge(account, challenge); final boolean challengeSuccess = pushChallengeManager.answerChallenge(account, challenge);
if (challengeSuccess) { if (challengeSuccess) {
rateLimiters.getPushChallengeSuccessLimiter().validate(account.getNumber()); rateLimiters.getPushChallengeSuccessLimiter().validate(account.getUuid());
resetRateLimits(account); resetRateLimits(account);
} }
} }
@ -67,7 +67,7 @@ public class RateLimitChallengeManager {
public void answerRecaptchaChallenge(final Account account, final String captcha, final String mostRecentProxyIp) public void answerRecaptchaChallenge(final Account account, final String captcha, final String mostRecentProxyIp)
throws RateLimitExceededException { throws RateLimitExceededException {
rateLimiters.getRecaptchaChallengeAttemptLimiter().validate(account.getNumber()); rateLimiters.getRecaptchaChallengeAttemptLimiter().validate(account.getUuid());
final boolean challengeSuccess = recaptchaClient.verify(captcha, mostRecentProxyIp); final boolean challengeSuccess = recaptchaClient.verify(captcha, mostRecentProxyIp);
@ -76,14 +76,14 @@ public class RateLimitChallengeManager {
SUCCESS_TAG_NAME, String.valueOf(challengeSuccess)).increment(); SUCCESS_TAG_NAME, String.valueOf(challengeSuccess)).increment();
if (challengeSuccess) { if (challengeSuccess) {
rateLimiters.getRecaptchaChallengeSuccessLimiter().validate(account.getNumber()); rateLimiters.getRecaptchaChallengeSuccessLimiter().validate(account.getUuid());
resetRateLimits(account); resetRateLimits(account);
} }
} }
private void resetRateLimits(final Account account) throws RateLimitExceededException { private void resetRateLimits(final Account account) throws RateLimitExceededException {
try { try {
rateLimiters.getRateLimitResetLimiter().validate(account.getNumber()); rateLimiters.getRateLimitResetLimiter().validate(account.getUuid());
} catch (final RateLimitExceededException e) { } catch (final RateLimitExceededException e) {
Metrics.counter(RESET_RATE_LIMIT_EXCEEDED_COUNTER_NAME, Metrics.counter(RESET_RATE_LIMIT_EXCEEDED_COUNTER_NAME,
SOURCE_COUNTRY_TAG_NAME, Util.getCountryCode(account.getNumber())).increment(); SOURCE_COUNTRY_TAG_NAME, Util.getCountryCode(account.getNumber())).increment();
@ -112,16 +112,14 @@ public class RateLimitChallengeManager {
public List<String> getChallengeOptions(final Account account) { public List<String> getChallengeOptions(final Account account) {
final List<String> options = new ArrayList<>(2); final List<String> options = new ArrayList<>(2);
final String key = account.getNumber(); if (rateLimiters.getRecaptchaChallengeAttemptLimiter().hasAvailablePermits(account.getUuid(), 1) &&
rateLimiters.getRecaptchaChallengeSuccessLimiter().hasAvailablePermits(account.getUuid(), 1)) {
if (rateLimiters.getRecaptchaChallengeAttemptLimiter().hasAvailablePermits(key, 1) &&
rateLimiters.getRecaptchaChallengeSuccessLimiter().hasAvailablePermits(key, 1)) {
options.add(OPTION_RECAPTCHA); options.add(OPTION_RECAPTCHA);
} }
if (rateLimiters.getPushChallengeAttemptLimiter().hasAvailablePermits(key, 1) && if (rateLimiters.getPushChallengeAttemptLimiter().hasAvailablePermits(account.getUuid(), 1) &&
rateLimiters.getPushChallengeSuccessLimiter().hasAvailablePermits(key, 1)) { rateLimiters.getPushChallengeSuccessLimiter().hasAvailablePermits(account.getUuid(), 1)) {
options.add(OPTION_PUSH_CHALLENGE); options.add(OPTION_PUSH_CHALLENGE);
} }

View File

@ -14,6 +14,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException; import java.io.IOException;
import java.time.Duration; import java.time.Duration;
import java.util.UUID;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration.RateLimitConfiguration; import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration.RateLimitConfiguration;
@ -61,14 +62,32 @@ public class RateLimiter {
} }
} }
public void validate(final UUID accountUuid) throws RateLimitExceededException {
validate(accountUuid.toString());
}
public void validate(final UUID sourceAccountUuid, final UUID destinationAccountUuid)
throws RateLimitExceededException {
validate(sourceAccountUuid.toString() + "__" + destinationAccountUuid.toString());
}
public void validate(String key) throws RateLimitExceededException { public void validate(String key) throws RateLimitExceededException {
validate(key, 1); validate(key, 1);
} }
public boolean hasAvailablePermits(final UUID accountUuid, final int permits) {
return hasAvailablePermits(accountUuid.toString(), permits);
}
public boolean hasAvailablePermits(final String key, final int permits) { public boolean hasAvailablePermits(final String key, final int permits) {
return getBucket(key).getTimeUntilSpaceAvailable(permits).equals(Duration.ZERO); return getBucket(key).getTimeUntilSpaceAvailable(permits).equals(Duration.ZERO);
} }
public void clear(final UUID accountUuid) {
clear(accountUuid.toString());
}
public void clear(String key) { public void clear(String key) {
cacheCluster.useCluster(connection -> connection.sync().del(getBucketName(key))); cacheCluster.useCluster(connection -> connection.sync().del(getBucketName(key)));
} }

View File

@ -65,7 +65,7 @@ public class UnsealedSenderRateLimiter {
try { try {
rateLimiters.getUnsealedSenderCardinalityLimiter() rateLimiters.getUnsealedSenderCardinalityLimiter()
.validate(sender.getNumber(), destination.getUuid().toString(), maxCardinality); .validate(sender.getUuid().toString(), destination.getUuid().toString(), maxCardinality);
} catch (final RateLimitExceededException e) { } catch (final RateLimitExceededException e) {
final boolean enforceLimit = dynamicConfigurationManager.getConfiguration() final boolean enforceLimit = dynamicConfigurationManager.getConfiguration()
@ -91,7 +91,7 @@ public class UnsealedSenderRateLimiter {
final long ttl; final long ttl;
{ {
final long remainingTtl = unsealedSenderCardinalityLimiter.getRemainingTtl(account.getNumber()); final long remainingTtl = unsealedSenderCardinalityLimiter.getRemainingTtl(account.getUuid().toString());
ttl = remainingTtl > 0 ? remainingTtl : unsealedSenderCardinalityLimiter.getInitialTtl().toSeconds(); ttl = remainingTtl > 0 ? remainingTtl : unsealedSenderCardinalityLimiter.getInitialTtl().toSeconds();
} }

View File

@ -49,7 +49,7 @@ class PreKeyRateLimiterTest {
void enforcementConfiguration() throws RateLimitExceededException { void enforcementConfiguration() throws RateLimitExceededException {
doThrow(RateLimitExceededException.class) doThrow(RateLimitExceededException.class)
.when(dailyPreKeyLimiter).validate(any()); .when(dailyPreKeyLimiter).validate(any(UUID.class));
when(rateLimitChallengeConfiguration.isPreKeyLimitEnforced()).thenReturn(false); when(rateLimitChallengeConfiguration.isPreKeyLimitEnforced()).thenReturn(false);

View File

@ -7,12 +7,13 @@ import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import com.vdurmont.semver4j.Semver; import com.vdurmont.semver4j.Semver;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
@ -66,6 +67,8 @@ class RateLimitChallengeManagerTest {
@ValueSource(booleans = {true, false}) @ValueSource(booleans = {true, false})
void answerPushChallenge(final boolean successfulChallenge) throws RateLimitExceededException { void answerPushChallenge(final boolean successfulChallenge) throws RateLimitExceededException {
final Account account = mock(Account.class); final Account account = mock(Account.class);
when(account.getUuid()).thenReturn(UUID.randomUUID());
when(pushChallengeManager.answerChallenge(eq(account), any())).thenReturn(successfulChallenge); when(pushChallengeManager.answerChallenge(eq(account), any())).thenReturn(successfulChallenge);
when(rateLimiters.getPushChallengeAttemptLimiter()).thenReturn(mock(RateLimiter.class)); when(rateLimiters.getPushChallengeAttemptLimiter()).thenReturn(mock(RateLimiter.class));
@ -78,8 +81,8 @@ class RateLimitChallengeManagerTest {
verify(preKeyRateLimiter).handleRateLimitReset(account); verify(preKeyRateLimiter).handleRateLimitReset(account);
verify(unsealedSenderRateLimiter).handleRateLimitReset(account); verify(unsealedSenderRateLimiter).handleRateLimitReset(account);
} else { } else {
verifyZeroInteractions(preKeyRateLimiter); verifyNoInteractions(preKeyRateLimiter);
verifyZeroInteractions(unsealedSenderRateLimiter); verifyNoInteractions(unsealedSenderRateLimiter);
} }
} }
@ -88,6 +91,7 @@ class RateLimitChallengeManagerTest {
void answerRecaptchaChallenge(final boolean successfulChallenge) throws RateLimitExceededException { void answerRecaptchaChallenge(final boolean successfulChallenge) throws RateLimitExceededException {
final Account account = mock(Account.class); final Account account = mock(Account.class);
when(account.getNumber()).thenReturn("+18005551234"); when(account.getNumber()).thenReturn("+18005551234");
when(account.getUuid()).thenReturn(UUID.randomUUID());
when(recaptchaClient.verify(any(), any())).thenReturn(successfulChallenge); when(recaptchaClient.verify(any(), any())).thenReturn(successfulChallenge);
@ -101,8 +105,8 @@ class RateLimitChallengeManagerTest {
verify(preKeyRateLimiter).handleRateLimitReset(account); verify(preKeyRateLimiter).handleRateLimitReset(account);
verify(unsealedSenderRateLimiter).handleRateLimitReset(account); verify(unsealedSenderRateLimiter).handleRateLimitReset(account);
} else { } else {
verifyZeroInteractions(preKeyRateLimiter); verifyNoInteractions(preKeyRateLimiter);
verifyZeroInteractions(unsealedSenderRateLimiter); verifyNoInteractions(unsealedSenderRateLimiter);
} }
} }
@ -150,14 +154,17 @@ class RateLimitChallengeManagerTest {
when(rateLimiters.getPushChallengeAttemptLimiter()).thenReturn(pushChallengeAttemptLimiter); when(rateLimiters.getPushChallengeAttemptLimiter()).thenReturn(pushChallengeAttemptLimiter);
when(rateLimiters.getPushChallengeSuccessLimiter()).thenReturn(pushChallengeSuccessLimiter); when(rateLimiters.getPushChallengeSuccessLimiter()).thenReturn(pushChallengeSuccessLimiter);
when(recaptchaChallengeAttemptLimiter.hasAvailablePermits(any(), anyInt())).thenReturn(captchaAttemptPermitted); when(recaptchaChallengeAttemptLimiter.hasAvailablePermits(any(UUID.class), anyInt())).thenReturn(captchaAttemptPermitted);
when(recaptchaChallengeSuccessLimiter.hasAvailablePermits(any(), anyInt())).thenReturn(captchaSuccessPermitted); when(recaptchaChallengeSuccessLimiter.hasAvailablePermits(any(UUID.class), anyInt())).thenReturn(captchaSuccessPermitted);
when(pushChallengeAttemptLimiter.hasAvailablePermits(any(), anyInt())).thenReturn(pushAttemptPermitted); when(pushChallengeAttemptLimiter.hasAvailablePermits(any(UUID.class), anyInt())).thenReturn(pushAttemptPermitted);
when(pushChallengeSuccessLimiter.hasAvailablePermits(any(), anyInt())).thenReturn(pushSuccessPermitted); when(pushChallengeSuccessLimiter.hasAvailablePermits(any(UUID.class), anyInt())).thenReturn(pushSuccessPermitted);
final int expectedLength = (expectCaptcha ? 1 : 0) + (expectPushChallenge ? 1 : 0); final int expectedLength = (expectCaptcha ? 1 : 0) + (expectPushChallenge ? 1 : 0);
final List<String> options = rateLimitChallengeManager.getChallengeOptions(mock(Account.class)); final Account account = mock(Account.class);
when(account.getUuid()).thenReturn(UUID.randomUUID());
final List<String> options = rateLimitChallengeManager.getChallengeOptions(account);
assertEquals(expectedLength, options.size()); assertEquals(expectedLength, options.size());
if (expectCaptcha) { if (expectCaptcha) {

View File

@ -60,7 +60,6 @@ import org.whispersystems.textsecuregcm.mappers.RateLimitChallengeExceptionMappe
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb; import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper; import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
@ -95,8 +94,6 @@ class KeysControllerTest {
private final static RateLimitChallengeManager rateLimitChallengeManager = mock(RateLimitChallengeManager.class ); private final static RateLimitChallengeManager rateLimitChallengeManager = mock(RateLimitChallengeManager.class );
private final static Account existsAccount = mock(Account.class ); private final static Account existsAccount = mock(Account.class );
private final static DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
private static final RateLimiters rateLimiters = mock(RateLimiters.class); private static final RateLimiters rateLimiters = mock(RateLimiters.class);
private static final RateLimiter rateLimiter = mock(RateLimiter.class ); private static final RateLimiter rateLimiter = mock(RateLimiter.class );
@ -105,7 +102,7 @@ class KeysControllerTest {
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(Account.class, DisabledPermittedAccount.class))) .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(Account.class, DisabledPermittedAccount.class)))
.setTestContainerFactory(new GrizzlyWebTestContainerFactory()) .setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new RateLimitChallengeExceptionMapper(rateLimitChallengeManager)) .addResource(new RateLimitChallengeExceptionMapper(rateLimitChallengeManager))
.addResource(new KeysController(rateLimiters, keysDynamoDb, accounts, preKeyRateLimiter, dynamicConfigurationManager, rateLimitChallengeManager)) .addResource(new KeysController(rateLimiters, keysDynamoDb, accounts, preKeyRateLimiter, rateLimitChallengeManager))
.build(); .build();
@BeforeEach @BeforeEach
@ -186,7 +183,6 @@ class KeysControllerTest {
existsAccount, existsAccount,
rateLimiters, rateLimiters,
rateLimiter, rateLimiter,
dynamicConfigurationManager,
rateLimitChallengeManager rateLimitChallengeManager
); );
} }

View File

@ -185,7 +185,7 @@ class ProfileControllerTest {
verify(accountsManager, times(1)).get(argThat((ArgumentMatcher<AmbiguousIdentifier>) identifier -> identifier != null && identifier.hasUuid() && identifier.getUuid().equals(AuthHelper.VALID_UUID_TWO))); verify(accountsManager, times(1)).get(argThat((ArgumentMatcher<AmbiguousIdentifier>) identifier -> identifier != null && identifier.hasUuid() && identifier.getUuid().equals(AuthHelper.VALID_UUID_TWO)));
verify(usernamesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO)); verify(usernamesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO));
verify(rateLimiter, times(1)).validate(eq(AuthHelper.VALID_NUMBER)); verify(rateLimiter, times(1)).validate(AuthHelper.VALID_UUID);
} }
@Test @Test
@ -206,7 +206,7 @@ class ProfileControllerTest {
verify(accountsManager, times(1)).get(argThat((ArgumentMatcher<AmbiguousIdentifier>) identifier -> identifier != null && identifier.hasNumber() && identifier.getNumber().equals(AuthHelper.VALID_NUMBER_TWO))); verify(accountsManager, times(1)).get(argThat((ArgumentMatcher<AmbiguousIdentifier>) identifier -> identifier != null && identifier.hasNumber() && identifier.getNumber().equals(AuthHelper.VALID_NUMBER_TWO)));
verifyNoMoreInteractions(usernamesManager); verifyNoMoreInteractions(usernamesManager);
verify(rateLimiter, times(1)).validate(eq(AuthHelper.VALID_NUMBER)); verify(rateLimiter, times(1)).validate(AuthHelper.VALID_UUID);
} }
@Test @Test
@ -225,7 +225,7 @@ class ProfileControllerTest {
verify(accountsManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO)); verify(accountsManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO));
verify(usernamesManager, times(1)).get(eq("n00bkiller")); verify(usernamesManager, times(1)).get(eq("n00bkiller"));
verify(usernameRateLimiter, times(1)).validate(eq(AuthHelper.VALID_UUID.toString())); verify(usernameRateLimiter, times(1)).validate(eq(AuthHelper.VALID_UUID));
} }
@Test @Test
@ -260,7 +260,7 @@ class ProfileControllerTest {
assertThat(response.getStatus()).isEqualTo(404); assertThat(response.getStatus()).isEqualTo(404);
verify(usernamesManager, times(1)).get(eq("n00bkillerzzzzz")); verify(usernamesManager, times(1)).get(eq("n00bkillerzzzzz"));
verify(usernameRateLimiter, times(1)).validate(eq(AuthHelper.VALID_UUID.toString())); verify(usernameRateLimiter, times(1)).validate(eq(AuthHelper.VALID_UUID));
} }
@ -587,7 +587,7 @@ class ProfileControllerTest {
verify(usernamesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO)); verify(usernamesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO));
verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion")); verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion"));
verify(rateLimiter, times(1)).validate(eq(AuthHelper.VALID_NUMBER)); verify(rateLimiter, times(1)).validate(AuthHelper.VALID_UUID);
} }
@Test @Test

View File

@ -6,7 +6,6 @@
package org.whispersystems.textsecuregcm.tests.controllers; package org.whispersystems.textsecuregcm.tests.controllers;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ -35,8 +34,8 @@ import org.whispersystems.textsecuregcm.util.SystemMapper;
@ExtendWith(DropwizardExtensionsSupport.class) @ExtendWith(DropwizardExtensionsSupport.class)
class StickerControllerTest { class StickerControllerTest {
private static RateLimiter rateLimiter = mock(RateLimiter.class ); private static final RateLimiter rateLimiter = mock(RateLimiter.class );
private static RateLimiters rateLimiters = mock(RateLimiters.class); private static final RateLimiters rateLimiters = mock(RateLimiters.class);
private static final ResourceExtension resources = ResourceExtension.builder() private static final ResourceExtension resources = ResourceExtension.builder()
.addProvider(AuthHelper.getAuthFilter()) .addProvider(AuthHelper.getAuthFilter())
@ -86,7 +85,7 @@ class StickerControllerTest {
} }
verify(rateLimiters, times(1)).getStickerPackLimiter(); verify(rateLimiters, times(1)).getStickerPackLimiter();
verify(rateLimiter, times(1)).validate(eq(AuthHelper.VALID_NUMBER)); verify(rateLimiter, times(1)).validate(AuthHelper.VALID_UUID);
} }
@Test @Test