Enforce phone number normalization when creating accounts or changing numbers

This commit is contained in:
Jon Chambers
2021-10-20 16:13:39 -04:00
committed by Jon Chambers
parent 7762afc497
commit 534c577f59
10 changed files with 235 additions and 91 deletions

View File

@@ -52,6 +52,7 @@ import java.util.concurrent.TimeUnit;
import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletRegistration;
import javax.ws.rs.ext.ExceptionMapper;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.glassfish.jersey.server.ServerProperties;
import org.jdbi.v3.core.Jdbi;
@@ -111,7 +112,9 @@ import org.whispersystems.textsecuregcm.limits.UnsealedSenderRateLimiter;
import org.whispersystems.textsecuregcm.liquibase.NameableMigrationsBundle;
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.InvalidWebsocketAddressExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.RateLimitChallengeExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.RetryLaterExceptionMapper;
@@ -708,29 +711,22 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
private void registerExceptionMappers(Environment environment,
WebSocketEnvironment<AuthenticatedAccount> webSocketEnvironment,
WebSocketEnvironment<AuthenticatedAccount> provisioningEnvironment) {
environment.jersey().register(new LoggingUnhandledExceptionMapper());
environment.jersey().register(new IOExceptionMapper());
environment.jersey().register(new RateLimitExceededExceptionMapper());
environment.jersey().register(new InvalidWebsocketAddressExceptionMapper());
environment.jersey().register(new DeviceLimitExceededExceptionMapper());
environment.jersey().register(new RetryLaterExceptionMapper());
environment.jersey().register(new ServerRejectedExceptionMapper());
webSocketEnvironment.jersey().register(new LoggingUnhandledExceptionMapper());
webSocketEnvironment.jersey().register(new IOExceptionMapper());
webSocketEnvironment.jersey().register(new RateLimitExceededExceptionMapper());
webSocketEnvironment.jersey().register(new InvalidWebsocketAddressExceptionMapper());
webSocketEnvironment.jersey().register(new DeviceLimitExceededExceptionMapper());
webSocketEnvironment.jersey().register(new RetryLaterExceptionMapper());
webSocketEnvironment.jersey().register(new ServerRejectedExceptionMapper());
provisioningEnvironment.jersey().register(new LoggingUnhandledExceptionMapper());
provisioningEnvironment.jersey().register(new IOExceptionMapper());
provisioningEnvironment.jersey().register(new RateLimitExceededExceptionMapper());
provisioningEnvironment.jersey().register(new InvalidWebsocketAddressExceptionMapper());
provisioningEnvironment.jersey().register(new DeviceLimitExceededExceptionMapper());
provisioningEnvironment.jersey().register(new RetryLaterExceptionMapper());
provisioningEnvironment.jersey().register(new ServerRejectedExceptionMapper());
List.of(
new LoggingUnhandledExceptionMapper(),
new IOExceptionMapper(),
new RateLimitExceededExceptionMapper(),
new InvalidWebsocketAddressExceptionMapper(),
new DeviceLimitExceededExceptionMapper(),
new RetryLaterExceptionMapper(),
new ServerRejectedExceptionMapper(),
new ImpossiblePhoneNumberExceptionMapper(),
new NonNormalizedPhoneNumberExceptionMapper()
).forEach(exceptionMapper -> {
environment.jersey().register(exceptionMapper);
webSocketEnvironment.jersey().register(exceptionMapper);
provisioningEnvironment.jersey().register(exceptionMapper);
});
}
private void registerCorsFilter(Environment environment) {

View File

@@ -80,6 +80,8 @@ import org.whispersystems.textsecuregcm.storage.UsernamesManager;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.ForwardedIpUtil;
import org.whispersystems.textsecuregcm.util.Hex;
import org.whispersystems.textsecuregcm.util.ImpossiblePhoneNumberException;
import org.whispersystems.textsecuregcm.util.NonNormalizedPhoneNumberException;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.VerificationCode;
@@ -160,18 +162,18 @@ public class AccountController {
@Timed
@GET
@Path("/{type}/preauth/{token}/{number}")
@Produces(MediaType.APPLICATION_JSON)
public Response getPreAuth(@PathParam("type") String pushType,
@PathParam("token") String pushToken,
@PathParam("number") String number,
@QueryParam("voip") Optional<Boolean> useVoip)
{
throws ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException {
if (!"apn".equals(pushType) && !"fcm".equals(pushType)) {
return Response.status(400).build();
}
if (!Util.isValidNumber(number)) {
return Response.status(400).build();
}
Util.requireNormalizedNumber(number);
String pushChallenge = generatePushChallenge();
StoredVerificationCode storedVerificationCode = new StoredVerificationCode(null,
@@ -195,6 +197,7 @@ public class AccountController {
@Timed
@GET
@Path("/{transport}/code/{number}")
@Produces(MediaType.APPLICATION_JSON)
public Response createAccount(@PathParam("transport") String transport,
@PathParam("number") String number,
@HeaderParam("X-Forwarded-For") String forwardedFor,
@@ -203,12 +206,9 @@ public class AccountController {
@QueryParam("client") Optional<String> client,
@QueryParam("captcha") Optional<String> captcha,
@QueryParam("challenge") Optional<String> pushChallenge)
throws RateLimitExceededException, RetryLaterException
{
if (!Util.isValidNumber(number)) {
logger.info("Invalid number: " + number);
throw new WebApplicationException(Response.status(400).build());
}
throws RateLimitExceededException, RetryLaterException, ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException {
Util.requireNormalizedNumber(number);
String sourceHost = ForwardedIpUtil.getMostRecentProxy(forwardedFor).orElseThrow();
@@ -340,6 +340,9 @@ public class AccountController {
rateLimiters.getVerifyLimiter().validate(number);
// Note that successful verification depends on being able to find a stored verification code for the given number.
// We check that numbers are normalized before we store verification codes, and so don't need to re-assert
// normalization here.
Optional<StoredVerificationCode> storedVerificationCode = pendingAccounts.getCodeForNumber(number);
if (storedVerificationCode.isEmpty() || !storedVerificationCode.get().isValid(verificationCode)) {
@@ -385,7 +388,7 @@ public class AccountController {
@Path("/number")
@Produces(MediaType.APPLICATION_JSON)
public void changeNumber(@Auth final AuthenticatedAccount authenticatedAccount, @Valid final ChangePhoneNumberRequest request)
throws RateLimitExceededException, InterruptedException {
throws RateLimitExceededException, InterruptedException, ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException {
if (request.getNumber().equals(authenticatedAccount.getAccount().getNumber())) {
// This may be a request that got repeated due to poor network conditions or other client error; take no action,
@@ -393,6 +396,8 @@ public class AccountController {
return;
}
Util.requireNormalizedNumber(request.getNumber());
rateLimiters.getVerifyLimiter().validate(request.getNumber());
final Optional<StoredVerificationCode> storedVerificationCode =

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.mappers;
import org.whispersystems.textsecuregcm.util.ImpossiblePhoneNumberException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
public class ImpossiblePhoneNumberExceptionMapper implements ExceptionMapper<ImpossiblePhoneNumberException> {
@Override
public Response toResponse(final ImpossiblePhoneNumberException exception) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.mappers;
import org.whispersystems.textsecuregcm.util.NonNormalizedPhoneNumberException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
public class NonNormalizedPhoneNumberExceptionMapper implements ExceptionMapper<NonNormalizedPhoneNumberException> {
@Override
public Response toResponse(final NonNormalizedPhoneNumberException exception) {
return Response.status(Status.BAD_REQUEST)
.entity(new NonNormalizedPhoneNumberResponse(exception.getOriginalNumber(), exception.getNormalizedNumber()))
.build();
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.mappers;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class NonNormalizedPhoneNumberResponse {
private final String originalNumber;
private final String normalizedNumber;
@JsonCreator
NonNormalizedPhoneNumberResponse(@JsonProperty("originalNumber") final String originalNumber,
@JsonProperty("normalizedNumber") final String normalizedNumber) {
this.originalNumber = originalNumber;
this.normalizedNumber = normalizedNumber;
}
public String getOriginalNumber() {
return originalNumber;
}
public String getNormalizedNumber() {
return normalizedNumber;
}
}

View File

@@ -5,13 +5,13 @@
package org.whispersystems.textsecuregcm.util;
public class ImpossibleNumberException extends Exception {
public class ImpossiblePhoneNumberException extends Exception {
public ImpossibleNumberException() {
public ImpossiblePhoneNumberException() {
super();
}
public ImpossibleNumberException(final Throwable cause) {
public ImpossiblePhoneNumberException(final Throwable cause) {
super(cause);
}
}

View File

@@ -5,12 +5,12 @@
package org.whispersystems.textsecuregcm.util;
public class NonNormalizedNumberException extends Exception {
public class NonNormalizedPhoneNumberException extends Exception {
private final String originalNumber;
private final String normalizedNumber;
public NonNormalizedNumberException(final String originalNumber, final String normalizedNumber) {
public NonNormalizedPhoneNumberException(final String originalNumber, final String normalizedNumber) {
this.originalNumber = originalNumber;
this.normalizedNumber = normalizedNumber;
}

View File

@@ -45,21 +45,17 @@ public class Util {
}
}
public static boolean isValidNumber(String number) {
return number.matches("^\\+[0-9]+") && PHONE_NUMBER_UTIL.isPossibleNumber(number, null);
}
/**
* Checks that the given number is a valid, E164-normalized phone number.
*
* @param number the number to check
*
* @throws ImpossibleNumberException if the given number is not a valid phone number at all
* @throws NonNormalizedNumberException if the given number is a valid phone number, but isn't E164-normalized
* @throws ImpossiblePhoneNumberException if the given number is not a valid phone number at all
* @throws NonNormalizedPhoneNumberException if the given number is a valid phone number, but isn't E164-normalized
*/
public static void requireNormalizedNumber(final String number) throws ImpossibleNumberException, NonNormalizedNumberException {
public static void requireNormalizedNumber(final String number) throws ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException {
if (!PHONE_NUMBER_UTIL.isPossibleNumber(number, null)) {
throw new ImpossibleNumberException();
throw new ImpossiblePhoneNumberException();
}
try {
@@ -67,10 +63,10 @@ public class Util {
final String normalizedNumber = PHONE_NUMBER_UTIL.format(phoneNumber, PhoneNumberFormat.E164);
if (!number.equals(normalizedNumber)) {
throw new NonNormalizedNumberException(number, normalizedNumber);
throw new NonNormalizedPhoneNumberException(number, normalizedNumber);
}
} catch (final NumberParseException e) {
throw new ImpossibleNumberException(e);
throw new ImpossiblePhoneNumberException(e);
}
}