From bb7d855aca655ac445997787eb16fa2a0e818680 Mon Sep 17 00:00:00 2001 From: Ameya Lokare Date: Tue, 14 Apr 2026 14:24:38 -0700 Subject: [PATCH] Prevent reg lock bypass on alternate phone number forms --- .../controllers/RegistrationController.java | 9 +++++- .../RegistrationControllerTest.java | 30 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java index f64cddd33..8f9f1602c 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java @@ -136,7 +136,14 @@ public class RegistrationController { final PhoneVerificationRequest.VerificationType verificationType = phoneVerificationTokenManager.verify(requestContext, number, registrationRequest); - final Optional existingAccount = accounts.getByE164(number); + // There can be at most one existing account for a set of numbers in the same equivalence class, so it's sufficient + // to find the first one. + final Optional existingAccount = Util.getAlternateForms(number) + .stream() + .map(accounts::getByE164) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); existingAccount.ifPresent(account -> { final Instant accountLastSeen = Instant.ofEpochMilli(account.getLastSeen()); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java index 9203b54d3..0a1641126 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java @@ -441,6 +441,36 @@ class RegistrationControllerTest { .argumentsForNextParameter(registrationLockErrors); } + @Test + void registrationLockOnAlternatePhoneNumberForm() throws Exception { + final String newFormatBeninNumber = PhoneNumberUtil.getInstance() + .format(PhoneNumberUtil.getInstance().getExampleNumber("BJ"), PhoneNumberUtil.PhoneNumberFormat.E164); + final String oldFormatBeninNumber = newFormatBeninNumber.replaceFirst("01", ""); + + when(registrationServiceClient.getSession(any(), any())) + .thenReturn( + CompletableFuture.completedFuture( + Optional.of(new RegistrationServiceSession(new byte[16], newFormatBeninNumber, true, null, null, null, + SESSION_EXPIRATION_SECONDS)))); + + final Account account = mock(Account.class); + when(accountsManager.getByE164(oldFormatBeninNumber)).thenReturn(Optional.of(account)); + when(accountsManager.getByE164(newFormatBeninNumber)).thenReturn(Optional.empty()); + when(account.hasCapability(DeviceCapability.TRANSFER)).thenReturn(false); + + doThrow(new WebApplicationException(RegistrationLockError.MISMATCH.getExpectedStatus())) + .when(registrationLockVerificationManager).verifyRegistrationLock(any(), any(), any(), any(), any()); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/registration") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(newFormatBeninNumber, PASSWORD)); + try (final Response response = request.post(Entity.json(requestJson("sessionId")))) { + assertEquals(RegistrationLockError.MISMATCH.getExpectedStatus(), response.getStatus()); + } + + } + @ParameterizedTest @CsvSource({