From 2f1843d5d2d1d8f581b666ec5b8d9466a3fbc596 Mon Sep 17 00:00:00 2001 From: Jon Chambers <63609320+jon-signal@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:41:51 -0400 Subject: [PATCH] Don't assume presence of phone numbers when checking for prohibited payment regions --- .../textsecuregcm/WhisperServerService.java | 6 +- .../dynamic/DynamicConfiguration.java | 2 +- .../dynamic/DynamicPaymentsConfiguration.java | 18 ++- .../controllers/ProfileController.java | 47 ++++-- .../grpc/ProfileGrpcService.java | 24 +-- .../textsecuregcm/util/ProfileHelper.java | 26 +++- .../main/proto/org/signal/chat/profile.proto | 5 +- .../dynamic/DynamicConfigurationTest.java | 4 +- .../controllers/ProfileControllerTest.java | 10 +- .../grpc/ProfileGrpcServiceTest.java | 15 +- .../textsecuregcm/util/ProfileHelperTest.java | 145 ++++++++++++++++++ 11 files changed, 242 insertions(+), 60 deletions(-) create mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/util/ProfileHelperTest.java diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 83fb539f0..63b15bc0a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -1093,7 +1093,7 @@ public class WhisperServerService extends Application disallowedPrefixes, + @JsonProperty @NotNull List disallowedAsnRegions) { - @JsonProperty - private List disallowedPrefixes = Collections.emptyList(); + public static DynamicPaymentsConfiguration DEFAULT = + new DynamicPaymentsConfiguration(Collections.emptyList(), Collections.emptyList()); - public List getDisallowedPrefixes() { - return disallowedPrefixes; + public DynamicPaymentsConfiguration { + if (disallowedPrefixes == null) { + disallowedPrefixes = DEFAULT.disallowedPrefixes(); + } + + if (disallowedAsnRegions == null) { + disallowedAsnRegions = DEFAULT.disallowedAsnRegions(); + } } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java index 6880a23e4..e32e9e776 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java @@ -50,6 +50,7 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.glassfish.jersey.server.ManagedAsync; @@ -64,6 +65,7 @@ import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialRespons import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest; import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations; +import org.whispersystems.textsecuregcm.asn.AsnInfoProvider; import org.whispersystems.textsecuregcm.auth.Anonymous; import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice; import org.whispersystems.textsecuregcm.auth.GroupSendTokenHeader; @@ -80,6 +82,7 @@ import org.whispersystems.textsecuregcm.entities.CreateProfileRequest; import org.whispersystems.textsecuregcm.entities.ExpiringProfileKeyCredentialProfileResponse; import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes; import org.whispersystems.textsecuregcm.entities.VersionedProfileResponse; +import org.whispersystems.textsecuregcm.filters.RemoteAddressFilter; import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; import org.whispersystems.textsecuregcm.identity.IdentityType; import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier; @@ -108,6 +111,7 @@ public class ProfileController { private final RateLimiters rateLimiters; private final ProfilesManager profilesManager; private final AccountsManager accountsManager; + private final Supplier asnInfoProviderSupplier; private final DynamicConfigurationManager dynamicConfigurationManager; private final ProfileBadgeConverter profileBadgeConverter; private final Map badgeConfigurationMap; @@ -124,25 +128,27 @@ public class ProfileController { private static final String DUPLICATE_AUTHENTICATION_COUNTER_NAME = name(ProfileController.class, "duplicateAuthentication"); public ProfileController( - Clock clock, - RateLimiters rateLimiters, - AccountsManager accountsManager, - ProfilesManager profilesManager, - DynamicConfigurationManager dynamicConfigurationManager, - ProfileBadgeConverter profileBadgeConverter, - BadgesConfiguration badgesConfiguration, - PostPolicyGenerator policyGenerator, - ServerSecretParams serverSecretParams, - ServerZkProfileOperations zkProfileOperations, - Executor batchIdentityCheckExecutor) { + final Clock clock, + final RateLimiters rateLimiters, + final AccountsManager accountsManager, + final ProfilesManager profilesManager, + final Supplier asnInfoProviderSupplier, + final DynamicConfigurationManager dynamicConfigurationManager, + final ProfileBadgeConverter profileBadgeConverter, + final BadgesConfiguration badgesConfiguration, + final PostPolicyGenerator policyGenerator, + final ServerSecretParams serverSecretParams, + final ServerZkProfileOperations zkProfileOperations, + final Executor batchIdentityCheckExecutor) { this.clock = clock; this.rateLimiters = rateLimiters; this.accountsManager = accountsManager; this.profilesManager = profilesManager; + this.asnInfoProviderSupplier = asnInfoProviderSupplier; this.dynamicConfigurationManager = dynamicConfigurationManager; this.profileBadgeConverter = profileBadgeConverter; - this.badgeConfigurationMap = badgesConfiguration.getBadges().stream().collect(Collectors.toMap( - BadgeConfiguration::getId, Function.identity())); + this.badgeConfigurationMap = badgesConfiguration.getBadges().stream() + .collect(Collectors.toMap(BadgeConfiguration::getId, Function.identity())); this.serverSecretParams = serverSecretParams; this.zkProfileOperations = zkProfileOperations; this.policyGenerator = policyGenerator; @@ -161,10 +167,12 @@ public class ProfileController { description = "If the request changed the avatar, the response body contains an upload form."))) @ApiResponse(responseCode = "400", description = "Invalid create profile request.") @ApiResponse(responseCode = "401", description = "Account authentication check failed.") - @ApiResponse(responseCode = "403", description = "The request contained a payment address, but payments are not supported in the region of the account’s phone number.") + @ApiResponse(responseCode = "403", description = "The request contained a payment address, but payments are not supported in the region of the account’s phone number or the caller's ASN if the account does not have a phone number.") @ApiResponse(responseCode = "412", description = "The requesting account has the profiles_v2 capability") @ApiResponse(responseCode = "422", description = "Invalid request format") - public Response setProfile(@Auth AuthenticatedDevice auth, @NotNull @Valid CreateProfileRequest request) { + public Response setProfile(@Auth AuthenticatedDevice auth, + @NotNull @Valid CreateProfileRequest request, + @Context final ContainerRequestContext requestContext) { final Account account = accountsManager.getByAccountIdentifier(auth.accountIdentifier()) .orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED)); @@ -176,7 +184,12 @@ public class ProfileController { final Optional currentProfile = profilesManager.getV1(auth.accountIdentifier(), request.version()); - if (request.paymentAddress() != null && request.paymentAddress().length != 0 && ProfileHelper.isPaymentAddressUpdateForbidden(account, Optional.empty(), currentProfile, dynamicConfigurationManager)) { + final String remoteAddress = (String) requestContext.getProperty(RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME); + + if (request.paymentAddress() != null && + request.paymentAddress().length != 0 && + ProfileHelper.isPaymentAddressUpdateForbidden(account, Optional.empty(), currentProfile, remoteAddress, asnInfoProviderSupplier.get(), dynamicConfigurationManager)) { + return Response.status(Response.Status.FORBIDDEN).build(); } @@ -198,7 +211,7 @@ public class ProfileController { currentAvatar.ifPresent(profilesManager::deleteAvatar); } - accountsManager.update(account.getIdentifier(IdentityType.ACI), a -> { + accountsManager.update(account.getAccountIdentifier(), a -> { final List updatedBadges = request.badges() .map(badges -> ProfileHelper.mergeBadgeIdsWithExistingAccountBadges(clock, badgeConfigurationMap, badges, a.getBadges())) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcService.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcService.java index 33a087617..6e23a58c2 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcService.java @@ -16,6 +16,7 @@ import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; import org.signal.chat.common.S3UploadForm; import org.signal.chat.errors.FailedPrecondition; @@ -37,8 +38,7 @@ import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.avatars.AvatarUploadCredentialRequest; import org.signal.libsignal.zkgroup.avatars.AvatarUploadCredentialResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.asn.AsnInfoProvider; import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice; import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil; import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter; @@ -46,7 +46,6 @@ import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration; import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration; import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; -import org.whispersystems.textsecuregcm.identity.IdentityType; import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator; @@ -63,11 +62,10 @@ import org.whispersystems.textsecuregcm.util.ProfileHelper; public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase { - private static final Logger logger = LoggerFactory.getLogger(ProfileGrpcService.class); - private final Clock clock; private final AccountsManager accountsManager; private final ProfilesManager profilesManager; + private final Supplier asnInfoProviderSupplier; private final DynamicConfigurationManager dynamicConfigurationManager; private final Map badgeConfigurationMap; private final PostPolicyGenerator policyGenerator; @@ -83,6 +81,7 @@ public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase { final Clock clock, final AccountsManager accountsManager, final ProfilesManager profilesManager, + final Supplier asnInfoProviderSupplier, final DynamicConfigurationManager dynamicConfigurationManager, final BadgesConfiguration badgesConfiguration, final PostPolicyGenerator policyGenerator, @@ -92,9 +91,10 @@ public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase { this.clock = clock; this.accountsManager = accountsManager; this.profilesManager = profilesManager; + this.asnInfoProviderSupplier = asnInfoProviderSupplier; this.dynamicConfigurationManager = dynamicConfigurationManager; - this.badgeConfigurationMap = badgesConfiguration.getBadges().stream().collect(Collectors.toMap( - BadgeConfiguration::getId, Function.identity())); + this.badgeConfigurationMap = badgesConfiguration.getBadges().stream() + .collect(Collectors.toMap(BadgeConfiguration::getId, Function.identity())); this.policyGenerator = policyGenerator; this.genericServerSecretParams = genericServerSecretParams; this.profileBadgeConverter = profileBadgeConverter; @@ -132,7 +132,9 @@ public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase { final Optional maybeV1Profile = profilesManager.getV1( authenticatedDevice.accountIdentifier(), HexFormat.of().formatHex(version)); - if (!request.getPaymentAddress().isEmpty() && ProfileHelper.isPaymentAddressUpdateForbidden(account, maybeProfile, maybeV1Profile, dynamicConfigurationManager)) { + if (!request.getPaymentAddress().isEmpty() && + ProfileHelper.isPaymentAddressUpdateForbidden(account, maybeProfile, maybeV1Profile, RequestAttributesUtil.getRemoteAddress().getHostAddress(), asnInfoProviderSupplier.get(), dynamicConfigurationManager)) { + return SetProfileResponse.newBuilder() .setPaymentsForbiddenInRegion(PaymentsForbiddenInRegion.getDefaultInstance()) .build(); @@ -172,7 +174,7 @@ public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase { ); try { - profilesManager.set(account.getIdentifier(IdentityType.ACI), v1Profile, profile, + profilesManager.set(account.getAccountIdentifier(), v1Profile, profile, request.getExpectedCurrentDataHash().isEmpty() ? null : request.getExpectedCurrentDataHash().toByteArray()); } catch (WriteConflictException _) { @@ -184,7 +186,7 @@ public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase { } try { - accountsManager.updateCurrentProfileVersion(account.getIdentifier(IdentityType.ACI), version, expectedCurrentVersion, a -> { + accountsManager.updateCurrentProfileVersion(account.getAccountIdentifier(), version, expectedCurrentVersion, a -> { final List updatedBadges = Optional.of(request.getBadgeIdsList()) .map(badges -> ProfileHelper.mergeBadgeIdsWithExistingAccountBadges(clock, badgeConfigurationMap, badges, @@ -235,7 +237,7 @@ public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase { request.getAvatarCredentialsRequest().toByteArray()); final AvatarUploadCredentialResponse credentialResponse = credentialRequest.issueCredential( - new ServiceId.Aci(account.getIdentifier(IdentityType.ACI)), + new ServiceId.Aci(account.getAccountIdentifier()), account.getZkCredentialKey().get(), Objects.requireNonNull(account.getZkCredentialKeyRotationId()), clock.instant().truncatedTo(ChronoUnit.DAYS), diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/util/ProfileHelper.java b/service/src/main/java/org/whispersystems/textsecuregcm/util/ProfileHelper.java index 988081253..a76593fef 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/util/ProfileHelper.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/util/ProfileHelper.java @@ -21,8 +21,10 @@ import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialRespons import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest; import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations; +import org.whispersystems.textsecuregcm.asn.AsnInfoProvider; import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration; import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicPaymentsConfiguration; import org.whispersystems.textsecuregcm.entities.CreateProfileRequest; import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; import org.whispersystems.textsecuregcm.storage.Account; @@ -41,12 +43,12 @@ public class ProfileHelper { final Map badgeConfigurationMap, final List badgeIds, final List accountBadges) { - LinkedHashMap existingBadges = new LinkedHashMap<>(accountBadges.size()); + final LinkedHashMap existingBadges = new LinkedHashMap<>(accountBadges.size()); for (final AccountBadge accountBadge : accountBadges) { existingBadges.putIfAbsent(accountBadge.id(), accountBadge); } - LinkedHashMap result = new LinkedHashMap<>(accountBadges.size()); + final LinkedHashMap result = new LinkedHashMap<>(accountBadges.size()); for (final String badgeId : badgeIds) { // duplicate in the list, ignore it @@ -104,17 +106,27 @@ public class ProfileHelper { @SuppressWarnings("OptionalUsedAsFieldOrParameterType") public static boolean isPaymentAddressUpdateForbidden( - final Account account, final Optional maybeProfile, + final Account account, + final Optional maybeProfile, final Optional maybeV1Profile, + final String ipAddress, + final AsnInfoProvider asnInfoProvider, final DynamicConfigurationManager dynamicConfigurationManager) { final Optional currentPaymentAddress = maybeProfile.map(VersionedProfile::paymentAddress) .or(() -> maybeV1Profile.map(VersionedProfileV1::paymentAddress)); - final boolean hasDisallowedPrefix = dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration() - .getDisallowedPrefixes().stream() - .anyMatch(prefix -> account.getNumber().startsWith(prefix)); - return hasDisallowedPrefix && currentPaymentAddress.filter(a -> a.length != 0).isEmpty(); + final DynamicPaymentsConfiguration paymentsConfiguration = + dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration(); + + final boolean hasDisallowedRegion = account.getNumberOptional() + .map(phoneNumber -> paymentsConfiguration.disallowedPrefixes().stream().anyMatch(phoneNumber::startsWith)) + .orElseGet(() -> asnInfoProvider.lookup(ipAddress) + .map(asnInfo -> paymentsConfiguration.disallowedAsnRegions().stream() + .anyMatch(region -> region.equalsIgnoreCase(asnInfo.regionCode()))) + .orElse(false)); + + return hasDisallowedRegion && currentPaymentAddress.filter(a -> a.length != 0).isEmpty(); } @Nullable diff --git a/service/src/main/proto/org/signal/chat/profile.proto b/service/src/main/proto/org/signal/chat/profile.proto index 3bda5faa0..b12ed1098 100644 --- a/service/src/main/proto/org/signal/chat/profile.proto +++ b/service/src/main/proto/org/signal/chat/profile.proto @@ -157,8 +157,9 @@ message SetProfileResponse { // The current data hash did not match the request's expectation, indicating // another device on the account may have written an update the caller does not know about. errors.FailedPrecondition expected_data_write_conflict = 2 [(tag.reason) = "expected_data_write_conflict"]; - // Payments are not permitted in the account's region, based on its phone number. - // The request should be retried without `payment_address. + // Payments are not permitted in the account's region, based on its phone + // number or the caller's IP address if the account does not have a phone + // number. The request should be retried without `payment_address. PaymentsForbiddenInRegion payments_forbidden_in_region = 3 [(tag.reason) = "payments_forbidden_in_region"]; // The current version did not match the request's expectation, indicating // another device on the account may have created a new version the caller does not know about. diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java index e5a1022a3..bdab13184 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java @@ -252,7 +252,7 @@ class DynamicConfigurationTest { final DynamicConfiguration emptyConfig = DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow(); - assertTrue(emptyConfig.getPaymentsConfiguration().getDisallowedPrefixes().isEmpty()); + assertTrue(emptyConfig.getPaymentsConfiguration().disallowedPrefixes().isEmpty()); } { @@ -266,7 +266,7 @@ class DynamicConfigurationTest { DynamicConfigurationManager.parseConfiguration(paymentsConfigYaml, DynamicConfiguration.class).orElseThrow() .getPaymentsConfiguration(); - assertEquals(List.of("+44"), config.getDisallowedPrefixes()); + assertEquals(List.of("+44"), config.disallowedPrefixes()); } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java index 53bd47375..9f9471f3f 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java @@ -79,6 +79,7 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequestContext; import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations; +import org.whispersystems.textsecuregcm.asn.AsnInfoProvider; import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice; import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil; import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter; @@ -161,6 +162,7 @@ class ProfileControllerTest { rateLimiters, accountsManager, profilesManager, + () -> AsnInfoProvider.EMPTY, dynamicConfigurationManager, new ProfileBadgeConverter() { @Override @@ -202,7 +204,7 @@ class ProfileControllerTest { when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); when(dynamicConfiguration.getPaymentsConfiguration()).thenReturn(dynamicPaymentsConfiguration); - when(dynamicPaymentsConfiguration.getDisallowedPrefixes()).thenReturn(Collections.emptyList()); + when(dynamicPaymentsConfiguration.disallowedPrefixes()).thenReturn(Collections.emptyList()); when(rateLimiters.getProfileLimiter()).thenReturn(rateLimiter); when(rateLimiters.getUsernameLookupLimiter()).thenReturn(usernameRateLimiter); @@ -212,7 +214,6 @@ class ProfileControllerTest { when(profileAccount.getIdentityKey(IdentityType.ACI)).thenReturn(ACCOUNT_TWO_IDENTITY_KEY); when(profileAccount.getIdentityKey(IdentityType.PNI)).thenReturn(ACCOUNT_TWO_PHONE_NUMBER_IDENTITY_KEY); when(profileAccount.getAccountIdentifier()).thenReturn(AuthHelper.VALID_UUID_TWO); - when(profileAccount.getIdentifier(IdentityType.ACI)).thenReturn(AuthHelper.VALID_UUID_TWO); when(profileAccount.getPhoneNumberIdentifier()).thenReturn(AuthHelper.VALID_PNI_TWO); when(profileAccount.getCurrentProfileVersion()).thenReturn(Optional.empty()); when(profileAccount.getUsernameHash()).thenReturn(Optional.of(USERNAME_HASH)); @@ -223,7 +224,6 @@ class ProfileControllerTest { capabilitiesAccount = mock(Account.class); when(capabilitiesAccount.getAccountIdentifier()).thenReturn(AuthHelper.VALID_UUID); - when(capabilitiesAccount.getIdentifier(IdentityType.ACI)).thenReturn(AuthHelper.VALID_UUID); when(capabilitiesAccount.getIdentityKey(IdentityType.ACI)).thenReturn(ACCOUNT_IDENTITY_KEY); when(capabilitiesAccount.getIdentityKey(IdentityType.PNI)).thenReturn(ACCOUNT_PHONE_NUMBER_IDENTITY_KEY); @@ -806,7 +806,7 @@ class ProfileControllerTest { @Test void testSetProfilePaymentAddressCountryNotAllowed() throws InvalidInputException { - when(dynamicPaymentsConfiguration.getDisallowedPrefixes()) + when(dynamicPaymentsConfiguration.disallowedPrefixes()) .thenReturn(List.of(AuthHelper.VALID_NUMBER_TWO.substring(0, 3))); final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID)); @@ -836,7 +836,7 @@ class ProfileControllerTest { @MethodSource void testSetProfilePaymentAddressCountryNotAllowedExistingPaymentAddress( @Nullable final byte[] existingPaymentAddressOnProfile, final byte[] requestPaymentAddress, final boolean expectAllowed) throws InvalidInputException { - when(dynamicPaymentsConfiguration.getDisallowedPrefixes()) + when(dynamicPaymentsConfiguration.disallowedPrefixes()) .thenReturn(List.of(AuthHelper.VALID_NUMBER_TWO.substring(0, 3))); final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID)); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcServiceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcServiceTest.java index 2b8ebdcc3..498f62aaf 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcServiceTest.java @@ -81,6 +81,7 @@ import org.signal.libsignal.zkgroup.avatars.AvatarUploadCredentialRequest; import org.signal.libsignal.zkgroup.avatars.AvatarUploadCredentialRequestContext; import org.signal.libsignal.zkgroup.avatars.AvatarUploadCredentialResponse; import org.signal.libsignal.zkgroup.profiles.ProfileKey; +import org.whispersystems.textsecuregcm.asn.AsnInfoProvider; import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessChecksum; import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil; import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter; @@ -183,8 +184,7 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest AsnInfoProvider.EMPTY, dynamicConfigurationManager, badgesConfiguration, policyGenerator, @@ -510,10 +512,9 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest dynamicConfigurationManager = + mock(DynamicConfigurationManager.class); + + when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); + + final Account account = mock(Account.class); + + switch (phoneNumberType) { + case ALLOWED -> when(account.getNumberOptional()) + .thenReturn(Optional.of(PhoneNumberUtil.getInstance().format( + PhoneNumberUtil.getInstance().getExampleNumber("DE"), PhoneNumberUtil.PhoneNumberFormat.E164))); + + case FORBIDDEN -> when(account.getNumberOptional()) + .thenReturn(Optional.of(PhoneNumberUtil.getInstance().format( + PhoneNumberUtil.getInstance().getExampleNumber("US"), PhoneNumberUtil.PhoneNumberFormat.E164))); + + case NONE -> when(account.getNumberOptional()).thenReturn(Optional.empty()); + } + + final AsnInfoProvider asnInfoProvider = mock(AsnInfoProvider.class); + + switch (ipAddressType) { + case ALLOWED -> when(asnInfoProvider.lookup(anyString())).thenReturn(Optional.of(new AsnInfo(123, "DE"))); + case FORBIDDEN -> when(asnInfoProvider.lookup(anyString())).thenReturn(Optional.of(new AsnInfo(123, "US"))); + case UNRECOGNIZED -> when(asnInfoProvider.lookup(anyString())).thenReturn(Optional.empty()); + } + + final Optional maybeProfile = switch (existingPaymentAddressType) { + case CURRENT_PROFILE -> Optional.of(new VersionedProfile(TestRandomUtil.nextBytes(16), + TestRandomUtil.nextBytes(16), + TestRandomUtil.nextBytes(16), + TestRandomUtil.nextBytes(16))); + case LEGACY_PROFILE, NONE -> Optional.empty(); + }; + + final Optional maybeLegacyProfile = switch (existingPaymentAddressType) { + case LEGACY_PROFILE -> Optional.of(new VersionedProfileV1("version", + TestRandomUtil.nextBytes(16), + "avatar", + TestRandomUtil.nextBytes(16), + TestRandomUtil.nextBytes(16), + TestRandomUtil.nextBytes(16), + TestRandomUtil.nextBytes(16), + TestRandomUtil.nextBytes(16))); + case CURRENT_PROFILE, NONE -> Optional.empty(); + }; + + assertEquals(expectForbidden, ProfileHelper.isPaymentAddressUpdateForbidden(account, + maybeProfile, + maybeLegacyProfile, + "127.0.0.1", + asnInfoProvider, + dynamicConfigurationManager)); + } + + private static List isPaymentAddressUpdateForbidden() { + return List.of( + Arguments.argumentSet("Permitted phone number", + PhoneNumberType.ALLOWED, IpAddressType.ALLOWED, ExistingPaymentAddressType.NONE, false), + + Arguments.argumentSet("Forbidden phone number", + PhoneNumberType.FORBIDDEN, IpAddressType.ALLOWED, ExistingPaymentAddressType.NONE, true), + + Arguments.argumentSet("Forbidden phone number, has existing address", + PhoneNumberType.FORBIDDEN, IpAddressType.ALLOWED, ExistingPaymentAddressType.CURRENT_PROFILE, false), + + Arguments.argumentSet("Forbidden phone number, has existing address in legacy profile", + PhoneNumberType.FORBIDDEN, IpAddressType.ALLOWED, ExistingPaymentAddressType.LEGACY_PROFILE, false), + + Arguments.argumentSet("No phone number, permitted ASN region", + PhoneNumberType.NONE, IpAddressType.ALLOWED, ExistingPaymentAddressType.NONE, false), + + Arguments.argumentSet("No phone number, unrecognized ASN region", + PhoneNumberType.NONE, IpAddressType.UNRECOGNIZED, ExistingPaymentAddressType.NONE, false), + + Arguments.argumentSet("No phone number, forbidden ASN region", + PhoneNumberType.NONE, IpAddressType.FORBIDDEN, ExistingPaymentAddressType.NONE, true), + + Arguments.argumentSet("No phone number, forbidden ASN region, has existing address", + PhoneNumberType.NONE, IpAddressType.FORBIDDEN, ExistingPaymentAddressType.CURRENT_PROFILE, false), + + Arguments.argumentSet("No phone number, forbidden ASN region, has existing address in legacy profile", + PhoneNumberType.NONE, IpAddressType.FORBIDDEN, ExistingPaymentAddressType.LEGACY_PROFILE, false) + ); + } +}