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 ec3863918..40974c4d8 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java @@ -130,6 +130,8 @@ public class AccountController { private static final String NONSTANDARD_USERNAME_COUNTER_NAME = name(AccountController.class, "nonStandardUsername"); + private static final String LOCKED_ACCOUNT_COUNTER_NAME = name(AccountController.class, "lockedAccount"); + private static final String CHALLENGE_PRESENT_TAG_NAME = "present"; private static final String CHALLENGE_MATCH_TAG_NAME = "matches"; private static final String COUNTRY_CODE_TAG_NAME = "countryCode"; @@ -142,6 +144,8 @@ public class AccountController { private static final String REGION_CODE_TAG_NAME = "regionCode"; private static final String VERIFICATION_TRANSPORT_TAG_NAME = "transport"; private static final String SCORE_TAG_NAME = "score"; + private static final String LOCK_REASON_TAG_NAME = "lockReason"; + private static final String ALREADY_LOCKED_TAG_NAME = "alreadyLocked"; private final StoredVerificationCodeManager pendingAccounts; @@ -812,11 +816,25 @@ 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, + // 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. - final Account updatedAccount = accounts.update(existingAccount, Account::lockAuthenticationCredentials); + boolean alreadyLocked = existingAccount.hasLockedCredentials(); + Metrics.counter(LOCKED_ACCOUNT_COUNTER_NAME, + LOCK_REASON_TAG_NAME, "verifiedNumberFailedReglock", + ALREADY_LOCKED_TAG_NAME, Boolean.toString(alreadyLocked)) + .increment(); + + final Account updatedAccount; + if (!alreadyLocked) { + updatedAccount = accounts.update(existingAccount, Account::lockAuthenticationCredentials); + } else { + updatedAccount = existingAccount; + } + List deviceIds = updatedAccount.getDevices().stream().map(Device::getId).toList(); clientPresenceManager.disconnectAllPresences(updatedAccount.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/storage/Account.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java index a43ed49fa..84b183794 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java @@ -472,6 +472,18 @@ public class Account { this.version = version; } + + /** + * Have all this account's devices been manually locked? + * + * @see Device#hasLockedCredentials + * + * @return true if all the account's devices were locked, false otherwise. + */ + public boolean hasLockedCredentials() { + return devices.stream().allMatch(Device::hasLockedCredentials); + } + /** * Lock account by invalidating authentication tokens. * 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 664adffcf..740418dd4 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,19 @@ public class Device { this.salt = credentials.getSalt(); } + /** + * Has this device been manually locked? + * + * We lock a device by prepending "!" to its token. + * This character cannot normally appear in valid tokens. + * + * @return true if the credential was locked, false otherwise. + */ + public boolean hasLockedCredentials() { + AuthenticationCredentials auth = getAuthenticationCredentials(); + return auth.getHashedAuthenticationToken().startsWith("!"); + } + /** * Lock device by invalidating authentication tokens. *