mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 23:38:03 +01:00
Lock account and send notification when someone passes phone verification but fails reglock
This commit is contained in:
@@ -552,7 +552,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
config.getDynamoDbTables().getSubscriptions().getTableName(), dynamoDbAsyncClient);
|
||||
|
||||
final RegistrationLockVerificationManager registrationLockVerificationManager = new RegistrationLockVerificationManager(
|
||||
accountsManager, clientPresenceManager, backupCredentialsGenerator, rateLimiters);
|
||||
accountsManager, clientPresenceManager, backupCredentialsGenerator, registrationRecoveryPasswordsManager, pushNotificationManager, rateLimiters);
|
||||
final PhoneVerificationTokenManager phoneVerificationTokenManager = new PhoneVerificationTokenManager(
|
||||
registrationServiceClient, registrationRecoveryPasswordsManager);
|
||||
|
||||
|
||||
@@ -21,11 +21,16 @@ import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
||||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
public class RegistrationLockVerificationManager {
|
||||
public enum Flow {
|
||||
@@ -40,23 +45,31 @@ public class RegistrationLockVerificationManager {
|
||||
name(RegistrationLockVerificationManager.class, "expiredRegistrationLock");
|
||||
private static final String REQUIRED_REGISTRATION_LOCK_COUNTER_NAME =
|
||||
name(RegistrationLockVerificationManager.class, "requiredRegistrationLock");
|
||||
private static final String CHALLENGED_DEVICE_NOT_PUSH_REGISTERED_COUNTER_NAME =
|
||||
name(RegistrationLockVerificationManager.class, "challengedDeviceNotPushRegistered");
|
||||
private static final String ALREADY_LOCKED_TAG_NAME = "alreadyLocked";
|
||||
private static final String REGISTRATION_LOCK_VERIFICATION_FLOW_TAG_NAME = "flow";
|
||||
private static final String REGISTRATION_LOCK_MATCHES_TAG_NAME = "registrationLockMatches";
|
||||
private static final String PHONE_VERIFICATION_TYPE_TAG_NAME = "phoneVerificationType";
|
||||
|
||||
|
||||
private final AccountsManager accounts;
|
||||
private final ClientPresenceManager clientPresenceManager;
|
||||
private final ExternalServiceCredentialsGenerator backupServiceCredentialGenerator;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
|
||||
private final PushNotificationManager pushNotificationManager;
|
||||
|
||||
public RegistrationLockVerificationManager(
|
||||
final AccountsManager accounts, final ClientPresenceManager clientPresenceManager,
|
||||
final ExternalServiceCredentialsGenerator backupServiceCredentialGenerator, final RateLimiters rateLimiters) {
|
||||
final ExternalServiceCredentialsGenerator backupServiceCredentialGenerator,
|
||||
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
|
||||
final PushNotificationManager pushNotificationManager,
|
||||
final RateLimiters rateLimiters) {
|
||||
this.accounts = accounts;
|
||||
this.clientPresenceManager = clientPresenceManager;
|
||||
this.backupServiceCredentialGenerator = backupServiceCredentialGenerator;
|
||||
this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
|
||||
this.pushNotificationManager = pushNotificationManager;
|
||||
this.rateLimiters = rateLimiters;
|
||||
}
|
||||
|
||||
@@ -125,20 +138,29 @@ public class RegistrationLockVerificationManager {
|
||||
// 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 ExternalServiceCredentials existingBackupCredentials =
|
||||
backupServiceCredentialGenerator.generateForUuid(account.getUuid());
|
||||
|
||||
final Account updatedAccount;
|
||||
if (!alreadyLocked) {
|
||||
updatedAccount = accounts.update(existingAccount, Account::lockAuthenticationCredentials);
|
||||
updatedAccount = accounts.update(account, Account::lockAuthTokenHash);
|
||||
} else {
|
||||
updatedAccount = existingAccount;
|
||||
updatedAccount = account;
|
||||
}
|
||||
|
||||
List<Long> deviceIds = updatedAccount.getDevices().stream().map(Device::getId).toList();
|
||||
// This will often be a no-op, since the recovery password is deleted when there's a verified session.
|
||||
// However, this covers the case where a user re-registers with SMS bypass and then forgets their PIN.
|
||||
registrationRecoveryPasswordsManager.removeForNumber(updatedAccount.getNumber());
|
||||
|
||||
final List<Long> deviceIds = updatedAccount.getDevices().stream().map(Device::getId).toList();
|
||||
clientPresenceManager.disconnectAllPresences(updatedAccount.getUuid(), deviceIds);
|
||||
*/
|
||||
final ExternalServiceCredentials existingBackupCredentials =
|
||||
backupServiceCredentialGenerator.generateForUuid(account.getUuid());
|
||||
|
||||
try {
|
||||
// Send a push notification that prompts the client to attempt login and fail due to locked credentials
|
||||
pushNotificationManager.sendAttemptLoginNotification(updatedAccount, "failedRegistrationLock");
|
||||
} catch (final NotPushRegisteredException e) {
|
||||
Metrics.counter(CHALLENGED_DEVICE_NOT_PUSH_REGISTERED_COUNTER_NAME).increment();
|
||||
}
|
||||
|
||||
throw new WebApplicationException(Response.status(FAILURE_HTTP_STATUS)
|
||||
.entity(new RegistrationLockFailure(existingRegistrationLock.getTimeRemaining().toMillis(),
|
||||
|
||||
@@ -96,6 +96,17 @@ public class APNSender implements Managed, PushNotificationSender {
|
||||
}
|
||||
}
|
||||
|
||||
case ATTEMPT_LOGIN_NOTIFICATION_HIGH_PRIORITY -> new SimpleApnsPayloadBuilder()
|
||||
.setMutableContent(true)
|
||||
.setLocalizedAlertMessage("APN_Message")
|
||||
.addCustomProperty("attemptLoginContext", notification.data())
|
||||
.build();
|
||||
|
||||
case ATTEMPT_LOGIN_NOTIFICATION_LOW_PRIORITY -> new SimpleApnsPayloadBuilder()
|
||||
.setContentAvailable(true)
|
||||
.addCustomProperty("attemptLoginContext", notification.data())
|
||||
.build();
|
||||
|
||||
case CHALLENGE -> new SimpleApnsPayloadBuilder()
|
||||
.setSound("default")
|
||||
.setLocalizedAlertMessage("APN_Message")
|
||||
|
||||
@@ -89,6 +89,7 @@ public class FcmSender implements PushNotificationSender {
|
||||
|
||||
final String key = switch (pushNotification.notificationType()) {
|
||||
case NOTIFICATION -> "notification";
|
||||
case ATTEMPT_LOGIN_NOTIFICATION_HIGH_PRIORITY, ATTEMPT_LOGIN_NOTIFICATION_LOW_PRIORITY -> "attemptLoginContext";
|
||||
case CHALLENGE -> "challenge";
|
||||
case RATE_LIMIT_CHALLENGE -> "rateLimitChallenge";
|
||||
};
|
||||
|
||||
@@ -18,7 +18,11 @@ public record PushNotification(String deviceToken,
|
||||
boolean urgent) {
|
||||
|
||||
public enum NotificationType {
|
||||
NOTIFICATION, CHALLENGE, RATE_LIMIT_CHALLENGE
|
||||
NOTIFICATION,
|
||||
ATTEMPT_LOGIN_NOTIFICATION_HIGH_PRIORITY,
|
||||
@Deprecated ATTEMPT_LOGIN_NOTIFICATION_LOW_PRIORITY, // Temporary support for iOS clients; can be removed after 2023-06-12
|
||||
CHALLENGE,
|
||||
RATE_LIMIT_CHALLENGE
|
||||
}
|
||||
|
||||
public enum TokenType {
|
||||
|
||||
@@ -78,6 +78,22 @@ public class PushNotificationManager {
|
||||
PushNotification.NotificationType.RATE_LIMIT_CHALLENGE, challengeToken, destination, device, true));
|
||||
}
|
||||
|
||||
public void sendAttemptLoginNotification(final Account destination, final String context) throws NotPushRegisteredException {
|
||||
final Device device = destination.getDevice(Device.MASTER_ID).orElseThrow(NotPushRegisteredException::new);
|
||||
final Pair<String, PushNotification.TokenType> tokenAndType = getToken(device);
|
||||
|
||||
sendNotification(new PushNotification(tokenAndType.first(), tokenAndType.second(),
|
||||
PushNotification.NotificationType.ATTEMPT_LOGIN_NOTIFICATION_HIGH_PRIORITY,
|
||||
context, destination, device, true));
|
||||
|
||||
// This is a workaround for older iOS clients who need a low priority push to trigger the logout notification
|
||||
if (tokenAndType.second() == PushNotification.TokenType.APN) {
|
||||
sendNotification(new PushNotification(tokenAndType.first(), tokenAndType.second(),
|
||||
PushNotification.NotificationType.ATTEMPT_LOGIN_NOTIFICATION_LOW_PRIORITY,
|
||||
context, destination, device, false));
|
||||
}
|
||||
}
|
||||
|
||||
public void handleMessagesRetrieved(final Account account, final Device device, final String userAgent) {
|
||||
RedisOperation.unchecked(() -> pushLatencyManager.recordQueueRead(account.getUuid(), device.getId(), userAgent));
|
||||
apnPushNotificationScheduler.cancelScheduledNotifications(account, device).whenComplete(logErrors());
|
||||
|
||||
@@ -14,6 +14,7 @@ import java.util.concurrent.CompletableFuture;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException;
|
||||
|
||||
public class RegistrationRecoveryPasswordsManager {
|
||||
|
||||
@@ -53,7 +54,10 @@ public class RegistrationRecoveryPasswordsManager {
|
||||
// there is no action to be taken on its completion
|
||||
return registrationRecoveryPasswords.removeEntry(number)
|
||||
.whenComplete((ignored, error) -> {
|
||||
if (error != null) {
|
||||
if (error instanceof ResourceNotFoundException) {
|
||||
// These will naturally happen if a recovery password is already deleted. Since we can remove
|
||||
// the recovery password through many flows, we avoid creating log messages for these exceptions
|
||||
} else if (error != null) {
|
||||
logger.warn("Failed to remove Registration Recovery Password", error);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user