From 0cb53f40f4c2c4eb01856af79a52e225b47530ab Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 12 Nov 2021 14:53:35 -0400 Subject: [PATCH] Update MobileCoin Payments Beta country codes. --- app/build.gradle | 2 +- .../securesms/keyvalue/PaymentsValues.java | 6 +- .../payments/GeographicalRestrictions.java | 182 +++++++++++++++--- .../securesms/util/FeatureFlags.java | 9 +- .../GeographicalRestrictionsTest.java | 49 ++++- 5 files changed, 211 insertions(+), 37 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c6a682493e..88351ce925 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -171,7 +171,7 @@ android { buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}' buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode" buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\"" - buildConfigField "int[]", "MOBILE_COIN_BLACKLIST", "new int[]{98,963,53,850,7}" + buildConfigField "int[]", "MOBILE_COIN_REGIONS", "new int[]{44,49,33,41}" buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\"" buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\"" diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.java index 8b7c83b791..d4d030f53d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.java @@ -108,15 +108,15 @@ public final class PaymentsValues extends SignalStoreValues { public PaymentsAvailability getPaymentsAvailability() { Context context = ApplicationDependencies.getApplication(); - if (!TextSecurePreferences.isPushRegistered(context) || - !GeographicalRestrictions.e164Allowed(TextSecurePreferences.getLocalNumber(context))) - { + if (!TextSecurePreferences.isPushRegistered(context)) { return PaymentsAvailability.NOT_IN_REGION; } if (FeatureFlags.payments()) { if (mobileCoinPaymentsEnabled()) { return PaymentsAvailability.WITHDRAW_AND_SEND; + } else if (!GeographicalRestrictions.e164Allowed(TextSecurePreferences.getLocalNumber(context))) { + return PaymentsAvailability.NOT_IN_REGION; } else { return PaymentsAvailability.REGISTRATION_AVAILABLE; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/GeographicalRestrictions.java b/app/src/main/java/org/thoughtcrime/securesms/payments/GeographicalRestrictions.java index 6708c206ab..1cada7839d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/GeographicalRestrictions.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/GeographicalRestrictions.java @@ -1,49 +1,185 @@ package org.thoughtcrime.securesms.payments; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberUtil; +import com.google.i18n.phonenumbers.Phonenumber; +import org.signal.core.util.MapUtil; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.BuildConfig; +import org.thoughtcrime.securesms.util.FeatureFlags; +import org.thoughtcrime.securesms.util.Util; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import io.reactivex.rxjava3.annotations.NonNull; + public final class GeographicalRestrictions { private static final String TAG = Log.tag(GeographicalRestrictions.class); - private GeographicalRestrictions() {} + private static final Validator BUILD_CONFIG_VALIDATOR = new BuildConfigWhitelistValidator(); - private static final Set BLACKLIST; - - static { - Set set = new HashSet<>(BuildConfig.MOBILE_COIN_BLACKLIST.length); - - for (int i = 0; i < BuildConfig.MOBILE_COIN_BLACKLIST.length; i++) { - set.add(BuildConfig.MOBILE_COIN_BLACKLIST[i]); - } - - BLACKLIST = Collections.unmodifiableSet(set); - } - - public static boolean regionAllowed(int regionCode) { - return !BLACKLIST.contains(regionCode); + private GeographicalRestrictions() { } public static boolean e164Allowed(@Nullable String e164) { - try { - int countryCode = PhoneNumberUtil.getInstance() - .parse(e164, null) - .getCountryCode(); + return selectValidator(FeatureFlags.mobileCoinBlacklist()).e164Allowed(e164); + } - return GeographicalRestrictions.regionAllowed(countryCode); - } catch (NumberParseException e) { - Log.w(TAG, e); - return false; + @VisibleForTesting + static boolean e164Allowed(@Nullable String e164, @Nullable String featureFlagBlacklist) { + return selectValidator(featureFlagBlacklist).e164Allowed(e164); + } + + public static @NonNull Validator selectValidator(@Nullable String featureFlagBlacklist) { + String[] blacklist = parseBlacklist(featureFlagBlacklist); + if (blacklist == null || blacklist.length == 0) { + return BUILD_CONFIG_VALIDATOR; + } else { + return new FeatureFlagBlacklistValidator(blacklist); } } + + public static @Nullable String[] parseBlacklist(@Nullable String featureFlagBlacklist) { + if (Util.isEmpty(featureFlagBlacklist)) { + return null; + } + + String[] parts = featureFlagBlacklist.split(","); + if (parts.length == 0) { + return null; + } + + return parts; + } + + private static class FeatureFlagBlacklistValidator implements Validator { + + private final Set blacklistCountries; + private final Map> blacklistRegions; + + private FeatureFlagBlacklistValidator(@NonNull String[] blacklist) { + Set countries = new HashSet<>(blacklist.length); + Map> regions = new HashMap<>(); + + for (final String entry : blacklist) { + try { + String[] parts = entry.trim().split(" "); + Integer countryCode = Integer.parseInt(parts[0].trim()); + + if (parts.length == 1) { + countries.add(countryCode); + } else if (parts.length == 2) { + Integer regionCode = Integer.parseInt(parts[1].trim()); + Set regionsForCountry = MapUtil.getOrDefault(regions, countryCode, new HashSet<>()); + + regionsForCountry.add(regionCode); + regions.put(countryCode, regionsForCountry); + } else { + Log.w(TAG, "Bad entry: " + entry.trim()); + } + } catch (NumberFormatException e) { + Log.w(TAG, "Failure parsing part", e); + } + } + + for (final Integer countryCode : regions.keySet()) { + Set regionCodes = regions.get(countryCode); + if (regionCodes != null) { + regions.put(countryCode, Collections.unmodifiableSet(regionCodes)); + } + } + + blacklistCountries = Collections.unmodifiableSet(countries); + blacklistRegions = Collections.unmodifiableMap(regions); + } + + @Override + public boolean e164Allowed(@Nullable String e164) { + try { + Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(e164, null); + + int countryCode = phoneNumber.getCountryCode(); + if (blacklistCountries.contains(countryCode)) { + return false; + } + + if (blacklistRegions.containsKey(countryCode)) { + Set regionsInCountry = blacklistRegions.get(countryCode); + if (regionsInCountry == null) { + return true; + } + + int nationalDestinationCodeLength = PhoneNumberUtil.getInstance().getLengthOfNationalDestinationCode(phoneNumber); + if (nationalDestinationCodeLength > 0) { + String nationalSignificantNumber = PhoneNumberUtil.getInstance().getNationalSignificantNumber(phoneNumber); + int nationalDestinationCode = Integer.parseInt(nationalSignificantNumber.substring(0, nationalDestinationCodeLength)); + + if (regionsInCountry.contains(nationalDestinationCode)) { + return false; + } + } + + int areaCodeLength = PhoneNumberUtil.getInstance().getLengthOfGeographicalAreaCode(phoneNumber); + if (areaCodeLength > 0) { + String nationalSignificantNumber = PhoneNumberUtil.getInstance().getNationalSignificantNumber(phoneNumber); + int areaCode = Integer.parseInt(nationalSignificantNumber.substring(0, areaCodeLength)); + + if (regionsInCountry.contains(areaCode)) { + return false; + } + } + } + + return true; + } catch (NumberParseException e) { + Log.w(TAG, e); + return false; + } + } + } + + private static class BuildConfigWhitelistValidator implements Validator { + private static final Set REGION_CODE_SET; + + static { + Set set = new HashSet<>(BuildConfig.MOBILE_COIN_REGIONS.length); + + for (int i = 0; i < BuildConfig.MOBILE_COIN_REGIONS.length; i++) { + set.add(BuildConfig.MOBILE_COIN_REGIONS[i]); + } + + REGION_CODE_SET = Collections.unmodifiableSet(set); + } + + private static boolean regionAllowed(int regionCode) { + return REGION_CODE_SET.contains(regionCode); + } + + @Override + public boolean e164Allowed(@Nullable String e164) { + try { + int countryCode = PhoneNumberUtil.getInstance() + .parse(e164, null) + .getCountryCode(); + + return regionAllowed(countryCode); + } catch (NumberParseException e) { + Log.w(TAG, e); + return false; + } + } + } + + private interface Validator { + boolean e164Allowed(@Nullable String e164); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 3edd292eaa..3c492ebe42 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.util; import android.text.TextUtils; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; @@ -88,6 +89,7 @@ public final class FeatureFlags { private static final String DONOR_BADGES_MEGAPHONE = "android.donorBadges.megaphone"; private static final String DONOR_BADGES_DISPLAY = "android.donorBadges.display"; private static final String CDSH = "android.cdsh"; + private static final String MOBILECOIN_BLACKLIST = "android.mobilecoin.blacklist"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -128,7 +130,8 @@ public final class FeatureFlags { SENDER_KEY_MAX_AGE, DONOR_BADGES, DONOR_BADGES_MEGAPHONE, - DONOR_BADGES_DISPLAY + DONOR_BADGES_DISPLAY, + MOBILECOIN_BLACKLIST ); @VisibleForTesting @@ -434,6 +437,10 @@ public final class FeatureFlags { return Environment.IS_STAGING && getBoolean(CDSH, false); } + public static @Nullable String mobileCoinBlacklist() { + return getString(MOBILECOIN_BLACKLIST, null); + } + /** Only for rendering debug info. */ public static synchronized @NonNull Map getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); diff --git a/app/src/test/java/org/thoughtcrime/securesms/payments/GeographicalRestrictionsTest.java b/app/src/test/java/org/thoughtcrime/securesms/payments/GeographicalRestrictionsTest.java index c1978d4e87..b874d446f9 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/payments/GeographicalRestrictionsTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/payments/GeographicalRestrictionsTest.java @@ -13,6 +13,8 @@ import static org.junit.Assume.assumeTrue; public final class GeographicalRestrictionsTest { + private static final String INVALID_BLACKLIST = "asdfkljsdhfla"; + @Before public void setup() { Log.initialize(new EmptyLogger()); @@ -20,28 +22,57 @@ public final class GeographicalRestrictionsTest { @Test public void bad_number_not_allowed() { - assertFalse(GeographicalRestrictions.e164Allowed("bad_number")); + assertFalse(GeographicalRestrictions.e164Allowed("bad_number", null)); } @Test public void null_not_allowed() { - assertFalse(GeographicalRestrictions.e164Allowed(null)); + assertFalse(GeographicalRestrictions.e164Allowed(null, null)); } @Test public void uk_allowed() { - assertTrue(GeographicalRestrictions.e164Allowed("+441617151234")); + assertTrue(GeographicalRestrictions.e164Allowed("+441617151234", null)); } @Test - public void crimea_not_allowed() { - assertFalse(GeographicalRestrictions.e164Allowed("+79782222222")); + public void us_not_allowed_in_release() { + assumeFalse(BuildConfig.DEBUG); + assertFalse(GeographicalRestrictions.e164Allowed("+15407011234", null)); } @Test - public void blacklist_not_allowed() { - for (int code : BuildConfig.MOBILE_COIN_BLACKLIST) { - assertFalse(GeographicalRestrictions.regionAllowed(code)); - } + public void givenAnInvalidBlackList_whenIE164AllowedAUkNumber_thenIExpectTrue() { + assertTrue(GeographicalRestrictions.e164Allowed("+441617151234", INVALID_BLACKLIST)); + } + + @Test + public void givenAValidBlacklistWithRegionBlock_whenIE164AllowedANumberInThatRegion_thenIExpectFalse() { + assertFalse(GeographicalRestrictions.e164Allowed("+73652222222", "7 365")); + } + + @Test + public void givenAValidBlacklistWithInvalidRegionBlock_whenIE164AllowedANumberInThatRegion_thenIExpectTrue() { + assertTrue(GeographicalRestrictions.e164Allowed("+73652222222", "4,7 365 2")); + } + + @Test + public void givenAValidBlacklist_whenIE164AllowedANumberNotInTheBlacklist_thenIExpectTrue() { + assertTrue(GeographicalRestrictions.e164Allowed("+73632222222", "4,7 365,44,33")); + } + + @Test + public void givenAValidBlacklist_whenIE164AllowedANumberInTheBlacklist_thenIExpectFalse() { + assertFalse(GeographicalRestrictions.e164Allowed("+73632222222", "4,7,44,33")); + } + + @Test + public void givenAValidBlacklistWithExtraSpaces_whenIE164AllowedANumberInTheBlacklist_thenIExpectFalse() { + assertFalse(GeographicalRestrictions.e164Allowed("+73632222222", " 4, 7, 44, 33 ")); + } + + @Test + public void givenAValidBlacklistWithAreaCode_whenIE164AllowedANumberInTheBlacklistAreaCode_thenIExpectFalse() { + assertFalse(GeographicalRestrictions.e164Allowed("+15065550199", "1 506")); } }