diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 4a6c92438..7614b92c4 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -654,7 +654,8 @@ public class WhisperServerService extends Application registrationLock, Optional registrationLockSalt, long lastSeen) { this.registrationLock = registrationLock; this.registrationLockSalt = registrationLockSalt; @@ -28,24 +42,22 @@ public class StoredRegistrationLock { } public boolean requiresClientRegistrationLock() { - return registrationLock.isPresent() && registrationLockSalt.isPresent() && System.currentTimeMillis() - lastSeen < TimeUnit.DAYS.toMillis(7); + boolean hasTimeRemaining = getTimeRemaining() >= 0; + return hasLockAndSalt() && hasTimeRemaining; } public boolean needsFailureCredentials() { - return registrationLock.isPresent() && registrationLockSalt.isPresent(); + return hasLockAndSalt(); } public long getTimeRemaining() { - return TimeUnit.DAYS.toMillis(7) - (System.currentTimeMillis() - lastSeen); + return TimeUnit.DAYS.toMillis(7) - timeSinceLastSeen(); } public boolean verify(@Nullable String clientRegistrationLock) { - if (Util.isEmpty(clientRegistrationLock)) { - return false; - } - - if (registrationLock.isPresent() && registrationLockSalt.isPresent() && !Util.isEmpty(clientRegistrationLock)) { - return new AuthenticationCredentials(registrationLock.get(), registrationLockSalt.get()).verify(clientRegistrationLock); + if (hasLockAndSalt() && Util.nonEmpty(clientRegistrationLock)) { + AuthenticationCredentials credentials = new AuthenticationCredentials(registrationLock.get(), registrationLockSalt.get()); + return credentials.verify(clientRegistrationLock); } else { return false; } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java index fca43db92..2a151a9aa 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java @@ -19,8 +19,10 @@ import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Tags; import java.security.SecureRandom; +import java.time.Clock; import java.time.Duration; import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -82,6 +84,7 @@ import org.whispersystems.textsecuregcm.entities.UsernameResponse; import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; import org.whispersystems.textsecuregcm.push.PushNotification; import org.whispersystems.textsecuregcm.push.PushNotificationManager; import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient; @@ -154,35 +157,64 @@ public class AccountController { private final ExternalServiceCredentialGenerator backupServiceCredentialGenerator; private final ChangeNumberManager changeNumberManager; + private final Clock clock; + + private final ClientPresenceManager clientPresenceManager; @VisibleForTesting static final Duration REGISTRATION_RPC_TIMEOUT = Duration.ofSeconds(15); - public AccountController(StoredVerificationCodeManager pendingAccounts, - AccountsManager accounts, - AbusiveHostRules abusiveHostRules, - RateLimiters rateLimiters, - RegistrationServiceClient registrationServiceClient, - DynamicConfigurationManager dynamicConfigurationManager, - TurnTokenGenerator turnTokenGenerator, - Map testDevices, - RecaptchaClient recaptchaClient, - PushNotificationManager pushNotificationManager, - ChangeNumberManager changeNumberManager, - ExternalServiceCredentialGenerator backupServiceCredentialGenerator) - { - this.pendingAccounts = pendingAccounts; - this.accounts = accounts; - this.abusiveHostRules = abusiveHostRules; - this.rateLimiters = rateLimiters; - this.registrationServiceClient = registrationServiceClient; - this.dynamicConfigurationManager = dynamicConfigurationManager; - this.testDevices = testDevices; - this.turnTokenGenerator = turnTokenGenerator; + public AccountController( + StoredVerificationCodeManager pendingAccounts, + AccountsManager accounts, + AbusiveHostRules abusiveHostRules, + RateLimiters rateLimiters, + RegistrationServiceClient registrationServiceClient, + DynamicConfigurationManager dynamicConfigurationManager, + TurnTokenGenerator turnTokenGenerator, + Map testDevices, + RecaptchaClient recaptchaClient, + PushNotificationManager pushNotificationManager, + ChangeNumberManager changeNumberManager, + ExternalServiceCredentialGenerator backupServiceCredentialGenerator, + ClientPresenceManager clientPresenceManager, + Clock clock + ) { + this.pendingAccounts = pendingAccounts; + this.accounts = accounts; + this.abusiveHostRules = abusiveHostRules; + this.rateLimiters = rateLimiters; + this.registrationServiceClient = registrationServiceClient; + this.dynamicConfigurationManager = dynamicConfigurationManager; + this.testDevices = testDevices; + this.turnTokenGenerator = turnTokenGenerator; this.recaptchaClient = recaptchaClient; - this.pushNotificationManager = pushNotificationManager; + this.pushNotificationManager = pushNotificationManager; this.backupServiceCredentialGenerator = backupServiceCredentialGenerator; this.changeNumberManager = changeNumberManager; + this.clientPresenceManager = clientPresenceManager; + this.clock = clock; + } + + @VisibleForTesting + public AccountController( + StoredVerificationCodeManager pendingAccounts, + AccountsManager accounts, + AbusiveHostRules abusiveHostRules, + RateLimiters rateLimiters, + RegistrationServiceClient registrationServiceClient, + DynamicConfigurationManager dynamicConfigurationManager, + TurnTokenGenerator turnTokenGenerator, + Map testDevices, + RecaptchaClient recaptchaClient, + PushNotificationManager pushNotificationManager, + ChangeNumberManager changeNumberManager, + ExternalServiceCredentialGenerator backupServiceCredentialGenerator + ) { + this(pendingAccounts, accounts, abusiveHostRules, rateLimiters, + registrationServiceClient, dynamicConfigurationManager, turnTokenGenerator, testDevices, recaptchaClient, + pushNotificationManager, changeNumberManager, + backupServiceCredentialGenerator, null, Clock.systemUTC()); } @Timed @@ -205,7 +237,7 @@ public class AccountController { String pushChallenge = generatePushChallenge(); StoredVerificationCode storedVerificationCode = - new StoredVerificationCode(null, System.currentTimeMillis(), pushChallenge, null, null); + new StoredVerificationCode(null, clock.millis(), pushChallenge, null, null); pendingAccounts.store(number, storedVerificationCode); pushNotificationManager.sendRegistrationChallengeNotification(pushToken, tokenType, storedVerificationCode.pushCode()); @@ -310,7 +342,7 @@ public class AccountController { messageTransport, clientType, acceptLanguage.orElse(null), REGISTRATION_RPC_TIMEOUT).join(); final StoredVerificationCode storedVerificationCode = new StoredVerificationCode(null, - System.currentTimeMillis(), + clock.millis(), maybeStoredVerificationCode.map(StoredVerificationCode::pushCode).orElse(null), null, sessionId); @@ -777,6 +809,12 @@ public class AccountController { } if (!existingRegistrationLock.verify(clientRegistrationLock)) { + // At this point, the client verified ownership of the phone number but doesn’t have the reglock PIN. + // Freezing the existing account credentials will definitively start the reglock timeout. Until the timeout, the current reglock can still be supplied, + // along with phone number verification, to restore access. + accounts.update(existingAccount, Account::lockAuthenticationCredentials); + List deviceIds = existingAccount.getDevices().stream().map(Device::getId).toList(); + clientPresenceManager.disconnectAllPresences(existingAccount.getUuid(), deviceIds); throw new WebApplicationException(Response.status(423) .entity(new RegistrationLockFailure(existingRegistrationLock.getTimeRemaining(), existingRegistrationLock.needsFailureCredentials() ? existingBackupCredentials : null)) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/push/ClientPresenceManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/push/ClientPresenceManager.java index 88bea9049..5495dc59b 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/push/ClientPresenceManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/push/ClientPresenceManager.java @@ -13,6 +13,8 @@ import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.Timer; import com.google.common.annotations.VisibleForTesting; import io.dropwizard.lifecycle.Managed; +import io.lettuce.core.LettuceFutures; +import io.lettuce.core.RedisFuture; import io.lettuce.core.ScriptOutputType; import io.lettuce.core.cluster.SlotHash; import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; @@ -21,6 +23,7 @@ import io.lettuce.core.cluster.models.partitions.RedisClusterNode; import io.lettuce.core.cluster.pubsub.RedisClusterPubSubAdapter; import java.io.IOException; import java.time.Duration; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Random; @@ -178,16 +181,25 @@ public class ClientPresenceManager extends RedisClusterPubSubAdapter deviceIds) { + + List presenceKeys = new ArrayList<>(); + deviceIds.forEach(deviceId -> { + String presenceKey = getPresenceKey(accountUuid, deviceId); + if (isLocallyPresent(accountUuid, deviceId)) { + displacePresence(presenceKey, false); + } + presenceKeys.add(presenceKey); + }); + + presenceCluster.useCluster(connection -> { + List> futures = presenceKeys.stream().map(key -> connection.async().del(key)).toList(); + LettuceFutures.awaitAll(connection.getTimeout(), futures.toArray(new RedisFuture[0])); + }); + } + public void disconnectPresence(final UUID accountUuid, final long deviceId) { - final String presenceKey = getPresenceKey(accountUuid, deviceId); - - if (isLocallyPresent(accountUuid, deviceId)) { - displacePresence(presenceKey, false); - } - - // If connected locally, we still need to clean up the presence key. - // If connected remotely, the other server will get a keyspace message and handle the disconnect - presenceCluster.useCluster(connection -> connection.sync().del(presenceKey)); + disconnectAllPresences(accountUuid, List.of(deviceId)); } private void displacePresence(final String presenceKey, final boolean connectedElsewhere) { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java index 582dac310..a43ed49fa 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java @@ -85,6 +85,7 @@ public class Account { @JsonIgnore private boolean canonicallyDiscoverable; + public UUID getUuid() { // this is the one method that may be called on a stale account return uuid; @@ -304,16 +305,10 @@ public class Account { public long getLastSeen() { requireNotStale(); - - long lastSeen = 0; - - for (Device device : devices) { - if (device.getLastSeen() > lastSeen) { - lastSeen = device.getLastSeen(); - } - } - - return lastSeen; + return devices.stream() + .map(Device::getLastSeen) + .max(Long::compare) + .orElse(0L); } public Optional getCurrentProfileVersion() { @@ -344,7 +339,6 @@ public class Account { public void addBadge(Clock clock, AccountBadge badge) { requireNotStale(); - boolean added = false; for (int i = 0; i < badges.size(); i++) { AccountBadge badgeInList = badges.get(i); @@ -478,6 +472,19 @@ public class Account { this.version = version; } + /** + * Lock account by invalidating authentication tokens. + * + * We only want to do this in cases where there is a potential conflict between the + * phone number holder and the registration lock holder. In that case, locking the + * account will ensure that either the registration lock holder proves ownership + * of the phone number, or after 7 days the phone number holder can register a new + * account. + */ + public void lockAuthenticationCredentials() { + devices.forEach(Device::lockAuthenticationCredentials); + } + boolean isStale() { return stale; } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java index 00dc189ba..664adffcf 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java @@ -149,6 +149,20 @@ public class Device { this.salt = credentials.getSalt(); } + /** + * Lock device by invalidating authentication tokens. + * + * This should only be used from Account::lockAuthenticationCredentials. + * + * See that method for more information. + */ + public void lockAuthenticationCredentials() { + AuthenticationCredentials oldAuth = getAuthenticationCredentials(); + String token = "!" + oldAuth.getHashedAuthenticationToken(); + String salt = oldAuth.getSalt(); + setAuthenticationCredentials(new AuthenticationCredentials(token, salt)); + } + public AuthenticationCredentials getAuthenticationCredentials() { return new AuthenticationCredentials(authToken, salt); } 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 41aa61449..26039fe9a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/util/Util.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/util/Util.java @@ -101,6 +101,10 @@ public class Util { return param == null || param.length() == 0; } + public static boolean nonEmpty(String param) { + return !isEmpty(param); + } + public static byte[] truncate(byte[] element, int length) { byte[] result = new byte[length]; System.arraycopy(element, 0, result, 0, result.length); @@ -138,15 +142,11 @@ public class Util { return parts; } - public static int toIntExact(long value) { - if ((int) value != value) { - throw new ArithmeticException("integer overflow"); - } - return (int) value; - } + public static final long DAY_IN_MILLIS = 86400000L; + public static final long WEEK_IN_MILLIS = DAY_IN_MILLIS * 7; public static int currentDaysSinceEpoch(@Nonnull Clock clock) { - return toIntExact(clock.millis() / 1000 / 60/ 60 / 24); + return Math.toIntExact(clock.millis() / DAY_IN_MILLIS); } public static void sleep(long i) { @@ -180,12 +180,12 @@ public class Util { } public static long todayInMillis(Clock clock) { - return TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(clock.instant().toEpochMilli())); + return TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(clock.millis())); } public static long todayInMillisGivenOffsetFromNow(Clock clock, Duration offset) { - final long currentTimeSeconds = offset.addTo(clock.instant()).getLong(ChronoField.INSTANT_SECONDS); - return TimeUnit.DAYS.toMillis(TimeUnit.SECONDS.toDays(currentTimeSeconds)); + final long ms = offset.toMillis() + clock.millis(); + return TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(ms)); } public static Optional findBestLocale(List priorityList, Collection supportedLocales) { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java index 84aa4fb34..cb159e0f9 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java @@ -13,6 +13,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.eq; @@ -22,7 +23,6 @@ import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -34,14 +34,12 @@ import com.google.i18n.phonenumbers.Phonenumber; import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; import io.dropwizard.testing.junit5.ResourceExtension; -import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.time.Duration; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -95,6 +93,7 @@ import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMa import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper; import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberResponse; import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; +import org.whispersystems.textsecuregcm.push.ClientPresenceManager; import org.whispersystems.textsecuregcm.push.PushNotification; import org.whispersystems.textsecuregcm.push.PushNotificationManager; import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient; @@ -114,6 +113,7 @@ import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; import org.whispersystems.textsecuregcm.tests.util.AuthHelper; import org.whispersystems.textsecuregcm.util.Hex; import org.whispersystems.textsecuregcm.util.SystemMapper; +import org.whispersystems.textsecuregcm.util.TestClock; @ExtendWith(DropwizardExtensionsSupport.class) class AccountControllerTest { @@ -146,26 +146,28 @@ class AccountControllerTest { private static final String TEST_NUMBER = "+14151111113"; private static StoredVerificationCodeManager pendingAccountsManager = mock(StoredVerificationCodeManager.class); - private static AccountsManager accountsManager = mock(AccountsManager.class); - private static AbusiveHostRules abusiveHostRules = mock(AbusiveHostRules.class); - private static RateLimiters rateLimiters = mock(RateLimiters.class); - private static RateLimiter rateLimiter = mock(RateLimiter.class); - private static RateLimiter pinLimiter = mock(RateLimiter.class); - private static RateLimiter smsVoiceIpLimiter = mock(RateLimiter.class); - private static RateLimiter smsVoicePrefixLimiter = mock(RateLimiter.class); - private static RateLimiter autoBlockLimiter = mock(RateLimiter.class); - private static RateLimiter usernameSetLimiter = mock(RateLimiter.class); - private static RateLimiter usernameReserveLimiter = mock(RateLimiter.class); - private static RateLimiter usernameLookupLimiter = mock(RateLimiter.class); + private static AccountsManager accountsManager = mock(AccountsManager.class); + private static AbusiveHostRules abusiveHostRules = mock(AbusiveHostRules.class); + private static RateLimiters rateLimiters = mock(RateLimiters.class); + private static RateLimiter rateLimiter = mock(RateLimiter.class); + private static RateLimiter pinLimiter = mock(RateLimiter.class); + private static RateLimiter smsVoiceIpLimiter = mock(RateLimiter.class); + private static RateLimiter smsVoicePrefixLimiter = mock(RateLimiter.class); + private static RateLimiter autoBlockLimiter = mock(RateLimiter.class); + private static RateLimiter usernameSetLimiter = mock(RateLimiter.class); + private static RateLimiter usernameReserveLimiter = mock(RateLimiter.class); + private static RateLimiter usernameLookupLimiter = mock(RateLimiter.class); private static RegistrationServiceClient registrationServiceClient = mock(RegistrationServiceClient.class); - private static TurnTokenGenerator turnTokenGenerator = mock(TurnTokenGenerator.class); - private static Account senderPinAccount = mock(Account.class); - private static Account senderRegLockAccount = mock(Account.class); - private static Account senderHasStorage = mock(Account.class); - private static Account senderTransfer = mock(Account.class); - private static RecaptchaClient recaptchaClient = mock(RecaptchaClient.class); + private static TurnTokenGenerator turnTokenGenerator = mock(TurnTokenGenerator.class); + private static Account senderPinAccount = mock(Account.class); + private static Account senderRegLockAccount = mock(Account.class); + private static Account senderHasStorage = mock(Account.class); + private static Account senderTransfer = mock(Account.class); + private static RecaptchaClient recaptchaClient = mock(RecaptchaClient.class); private static PushNotificationManager pushNotificationManager = mock(PushNotificationManager.class); - private static ChangeNumberManager changeNumberManager = mock(ChangeNumberManager.class); + private static ChangeNumberManager changeNumberManager = mock(ChangeNumberManager.class); + private static ClientPresenceManager clientPresenceManager = mock(ClientPresenceManager.class); + private static TestClock testClock = TestClock.now(); private static DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class); @@ -194,7 +196,9 @@ class AccountControllerTest { recaptchaClient, pushNotificationManager, changeNumberManager, - storageCredentialGenerator)) + storageCredentialGenerator, + clientPresenceManager, + testClock)) .build(); @@ -340,7 +344,8 @@ class AccountControllerTest { senderTransfer, recaptchaClient, pushNotificationManager, - changeNumberManager); + changeNumberManager, + clientPresenceManager); clearInvocations(AuthHelper.DISABLED_DEVICE); } @@ -1063,6 +1068,8 @@ class AccountControllerTest { assertThat(response.getStatus()).isEqualTo(423); + verify(senderRegLockAccount).lockAuthenticationCredentials(); + verify(clientPresenceManager, times(1)).disconnectAllPresences(eq(SENDER_REG_LOCK_UUID), any()); verify(pinLimiter).validate(eq(SENDER_REG_LOCK)); } @@ -1085,6 +1092,8 @@ class AccountControllerTest { assertThat(failure.getBackupCredentials().getPassword().startsWith(SENDER_REG_LOCK_UUID.toString())).isTrue(); assertThat(failure.getTimeRemaining()).isGreaterThan(0); + verify(senderRegLockAccount).lockAuthenticationCredentials(); + verify(clientPresenceManager, atLeastOnce()).disconnectAllPresences(eq(SENDER_REG_LOCK_UUID), any()); verifyNoInteractions(pinLimiter); } @@ -1311,9 +1320,10 @@ class AccountControllerTest { final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class); when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(true); + final UUID existingUuid = UUID.randomUUID(); final Account existingAccount = mock(Account.class); when(existingAccount.getNumber()).thenReturn(number); - when(existingAccount.getUuid()).thenReturn(UUID.randomUUID()); + when(existingAccount.getUuid()).thenReturn(existingUuid); when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock); when(accountsManager.getByE164(number)).thenReturn(Optional.of(existingAccount)); @@ -1327,6 +1337,9 @@ class AccountControllerTest { MediaType.APPLICATION_JSON_TYPE)); assertThat(response.getStatus()).isEqualTo(423); + + verify(existingAccount).lockAuthenticationCredentials(); + verify(clientPresenceManager, atLeastOnce()).disconnectAllPresences(eq(existingUuid), any()); verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any(), any(), any()); } @@ -1347,9 +1360,10 @@ class AccountControllerTest { when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(true); when(existingRegistrationLock.verify(anyString())).thenReturn(false); + UUID existingUuid = UUID.randomUUID(); final Account existingAccount = mock(Account.class); when(existingAccount.getNumber()).thenReturn(number); - when(existingAccount.getUuid()).thenReturn(UUID.randomUUID()); + when(existingAccount.getUuid()).thenReturn(existingUuid); when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock); when(accountsManager.getByE164(number)).thenReturn(Optional.of(existingAccount)); @@ -1363,6 +1377,9 @@ class AccountControllerTest { MediaType.APPLICATION_JSON_TYPE)); assertThat(response.getStatus()).isEqualTo(423); + + verify(existingAccount).lockAuthenticationCredentials(); + verify(clientPresenceManager, atLeastOnce()).disconnectAllPresences(eq(existingUuid), any()); verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any(), any(), any()); } @@ -1399,6 +1416,8 @@ class AccountControllerTest { MediaType.APPLICATION_JSON_TYPE)); assertThat(response.getStatus()).isEqualTo(200); + verify(senderRegLockAccount, never()).lockAuthenticationCredentials(); + verify(clientPresenceManager, never()).disconnectAllPresences(eq(SENDER_REG_LOCK_UUID), any()); verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), any(), any(), any(), any(), any()); } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AccountsHelper.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AccountsHelper.java index e669a48f8..2e0d05558 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AccountsHelper.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AccountsHelper.java @@ -132,6 +132,7 @@ public class AccountsHelper { case "getRegistrationLock" -> when(updatedAccount.getRegistrationLock()).thenAnswer(stubbing); case "getIdentityKey" -> when(updatedAccount.getIdentityKey()).thenAnswer(stubbing); case "getBadges" -> when(updatedAccount.getBadges()).thenAnswer(stubbing); + case "getLastSeen" -> when(updatedAccount.getLastSeen()).thenAnswer(stubbing); default -> throw new IllegalArgumentException("unsupported method: Account#" + stubbing.getInvocation().getMethod().getName()); } }