diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/util/ImpossibleNumberException.java b/service/src/main/java/org/whispersystems/textsecuregcm/util/ImpossibleNumberException.java new file mode 100644 index 000000000..a07ffe714 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/util/ImpossibleNumberException.java @@ -0,0 +1,17 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +public class ImpossibleNumberException extends Exception { + + public ImpossibleNumberException() { + super(); + } + + public ImpossibleNumberException(final Throwable cause) { + super(cause); + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/util/NonNormalizedNumberException.java b/service/src/main/java/org/whispersystems/textsecuregcm/util/NonNormalizedNumberException.java new file mode 100644 index 000000000..c7e821097 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/util/NonNormalizedNumberException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +public class NonNormalizedNumberException extends Exception { + + private final String originalNumber; + private final String normalizedNumber; + + public NonNormalizedNumberException(final String originalNumber, final String normalizedNumber) { + this.originalNumber = originalNumber; + this.normalizedNumber = normalizedNumber; + } + + public String getOriginalNumber() { + return originalNumber; + } + + public String getNormalizedNumber() { + return normalizedNumber; + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/util/Util.java b/service/src/main/java/org/whispersystems/textsecuregcm/util/Util.java index 1f67bb3cf..02b0d6e51 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/util/Util.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/util/Util.java @@ -4,7 +4,10 @@ */ package org.whispersystems.textsecuregcm.util; +import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberUtil; +import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat; +import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.MessageDigest; @@ -28,6 +31,8 @@ public class Util { private static final Pattern COUNTRY_CODE_PATTERN = Pattern.compile("^\\+([17]|2[07]|3[0123469]|4[013456789]|5[12345678]|6[0123456]|8[1246]|9[0123458]|\\d{3})"); + private static final PhoneNumberUtil PHONE_NUMBER_UTIL = PhoneNumberUtil.getInstance(); + public static byte[] getContactToken(String number) { try { MessageDigest digest = MessageDigest.getInstance("SHA1"); @@ -41,7 +46,32 @@ public class Util { } public static boolean isValidNumber(String number) { - return number.matches("^\\+[0-9]+") && PhoneNumberUtil.getInstance().isPossibleNumber(number, null); + 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 + */ + public static void requireNormalizedNumber(final String number) throws ImpossibleNumberException, NonNormalizedNumberException { + if (!PHONE_NUMBER_UTIL.isPossibleNumber(number, null)) { + throw new ImpossibleNumberException(); + } + + try { + final PhoneNumber phoneNumber = PHONE_NUMBER_UTIL.parse(number, null); + final String normalizedNumber = PHONE_NUMBER_UTIL.format(phoneNumber, PhoneNumberFormat.E164); + + if (!number.equals(normalizedNumber)) { + throw new NonNormalizedNumberException(number, normalizedNumber); + } + } catch (final NumberParseException e) { + throw new ImpossibleNumberException(e); + } } public static String getCountryCode(String number) { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/ValidNumberTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/ValidNumberTest.java index 1c3366e8a..7f07c727c 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/ValidNumberTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/ValidNumberTest.java @@ -5,14 +5,20 @@ package org.whispersystems.textsecuregcm.tests.util; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.whispersystems.textsecuregcm.util.ImpossibleNumberException; +import org.whispersystems.textsecuregcm.util.NonNormalizedNumberException; import org.whispersystems.textsecuregcm.util.Util; import java.util.stream.Stream; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; class ValidNumberTest { @@ -53,4 +59,51 @@ class ValidNumberTest { Arguments.of("+689123456", true) ); } + + @ParameterizedTest + @ValueSource(strings = { + "+447700900111", + "+14151231234", + "+71234567890", + "+447535742222", + "+4915174108888", + "+298123456", + "+299123456", + "+376123456", + "+68512345", + "+689123456"}) + void requireNormalizedNumber(final String number) { + assertDoesNotThrow(() -> Util.requireNormalizedNumber(number)); + } + + @Test + void requireNormalizedNumberNull() { + assertThrows(ImpossibleNumberException.class, () -> Util.requireNormalizedNumber(null)); + } + + @ParameterizedTest + @ValueSource(strings = { + "Definitely not a phone number at all", + "+141512312341", + "+712345678901", + "+4475357422221", + "+491517410888811111", + "71234567890", + "001447535742222", + "+1415123123a" + }) + void requireNormalizedNumberImpossibleNumber(final String number) { + assertThrows(ImpossibleNumberException.class, () -> Util.requireNormalizedNumber(number)); + } + + @ParameterizedTest + @ValueSource(strings = { + "+4407700900111", + "+1 415 123 1234", + "+1 (415) 123-1234", + "+1 415)123-1234", + " +14151231234"}) + void requireNormalizedNumberNonNormalized(final String number) { + assertThrows(NonNormalizedNumberException.class, () -> Util.requireNormalizedNumber(number)); + } }