diff --git a/Signal/Registration/RegistrationCoordinatorBackupErrorPresenter.swift b/Signal/Registration/RegistrationCoordinatorBackupErrorPresenter.swift index 1fb014abec..6ac6f4ff23 100644 --- a/Signal/Registration/RegistrationCoordinatorBackupErrorPresenter.swift +++ b/Signal/Registration/RegistrationCoordinatorBackupErrorPresenter.swift @@ -57,6 +57,11 @@ public class RegistrationCoordinatorBackupErrorPresenterImpl: case _ where error is SignalError: // LibSignalError (e.g. - creating credentials) return .incorrectRecoveryKey + case let httpError as OWSHTTPError where httpError.responseStatusCode == 401: + // Incorrect AEP + // -- and/or -- + // No public key registered for this backupID (AEP+ACI) + return .incorrectRecoveryKey case let httpError as OWSHTTPError where httpError.responseStatusCode == 404: // No backup found in the CDN return .backupNotFound @@ -171,7 +176,7 @@ public class RegistrationCoordinatorBackupErrorPresenterImpl: continuation.resume(returning: .skipRestore) }) actions.append(ActionSheetAction(title: tryAgainString) { _ in - continuation.resume(returning: .tryAgain) + continuation.resume(returning: .incorrectRecoveryKey) }) case .incorrectRecoveryKey: title = OWSLocalizedString( diff --git a/Signal/Registration/RegistrationCoordinatorDependencies.swift b/Signal/Registration/RegistrationCoordinatorDependencies.swift index 0b4e7a2bad..34be537a10 100644 --- a/Signal/Registration/RegistrationCoordinatorDependencies.swift +++ b/Signal/Registration/RegistrationCoordinatorDependencies.swift @@ -11,6 +11,7 @@ public struct RegistrationCoordinatorDependencies { public let accountEntropyPoolGenerator: () -> AccountEntropyPool public let accountKeyStore: AccountKeyStore public let backupArchiveManager: BackupArchiveManager + public let backupIdService: BackupIdService public let backupNonceStore: BackupNonceMetadataStore public let backupRequestManager: BackupRequestManager public let changeNumberPniManager: ChangePhoneNumberPniManager @@ -53,6 +54,7 @@ public struct RegistrationCoordinatorDependencies { accountEntropyPoolGenerator: { AccountEntropyPool() }, accountKeyStore: DependenciesBridge.shared.accountKeyStore, backupArchiveManager: DependenciesBridge.shared.backupArchiveManager, + backupIdService: DependenciesBridge.shared.backupIdService, backupNonceStore: BackupNonceMetadataStore(), backupRequestManager: DependenciesBridge.shared.backupRequestManager, changeNumberPniManager: DependenciesBridge.shared.changePhoneNumberPniManager, diff --git a/Signal/Registration/RegistrationCoordinatorImpl.swift b/Signal/Registration/RegistrationCoordinatorImpl.swift index d62ebb0b9a..e313377ddc 100644 --- a/Signal/Registration/RegistrationCoordinatorImpl.swift +++ b/Signal/Registration/RegistrationCoordinatorImpl.swift @@ -598,10 +598,9 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator { // TODO: [Backups] This is currently unsupported, so log and return throw OWSAssertionError("Local backups not supported.") case .remote: - let backupServiceAuth = try await self.deps.backupRequestManager.fetchBackupServiceAuthForRegistration( - key: backupKey, - localAci: identity.aci, - chatServiceAuth: identity.chatServiceAuth + let backupServiceAuth = try await self.fetchBackupServiceAuth( + accountEntropyPool: accountEntropyPool, + accountIdentity: identity ) fileUrl = try await self.deps.backupArchiveManager.downloadEncryptedBackup( backupKey: backupKey, @@ -644,10 +643,9 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator { nonceSource = .svr🐝(header: metadataHeader, auth: identity.chatServiceAuth) } else { Logger.info("Missing metadata header; refetching from cdn") - let backupServiceAuth = try await self.deps.backupRequestManager.fetchBackupServiceAuthForRegistration( - key: backupKey, - localAci: identity.aci, - chatServiceAuth: identity.chatServiceAuth + let backupServiceAuth = try await self.fetchBackupServiceAuth( + accountEntropyPool: accountEntropyPool, + accountIdentity: identity ) let metadataHeader = try await self.deps.backupArchiveManager.backupCdnInfo( backupKey: backupKey, @@ -1472,10 +1470,9 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator { do { // For manual restore, fetch the backup info let backupKey = try MessageRootBackupKey(accountEntropyPool: accountEntropyPool, aci: accountIdentity.aci) - let backupServiceAuth = try await self.deps.backupRequestManager.fetchBackupServiceAuthForRegistration( - key: backupKey, - localAci: accountIdentity.aci, - chatServiceAuth: accountIdentity.chatServiceAuth + let backupServiceAuth = try await self.fetchBackupServiceAuth( + accountEntropyPool: accountEntropyPool, + accountIdentity: accountIdentity ) let cdnInfo = try await self.deps.backupArchiveManager.backupCdnInfo( backupKey: backupKey, @@ -1500,11 +1497,6 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator { switch step { case .incorrectRecoveryKey: - if self.persistedState.restoreMode == .manualRestore { - // If manual restore, there's not much of a recovery path here - // so just skip restoring and continue - return await updateRestoreMethod(method: .declined).awaitable() - } return .enterRecoveryKey( RegistrationEnterAccountEntropyPoolState( canShowBackButton: persistedState.accountIdentity == nil @@ -1517,6 +1509,38 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator { } } + /// It is possible that, in the time between the last backup and this restore, + /// the user has registered without restoring. This can result in the AEP being + /// rotated and a new ACI+AEP backupId being registered. If this happens, + /// fetching auth credentials using the original AEP will fail. + /// The good news is this may be recoverable by re-registering the passed in ACI+AEP + /// backupId as the current backupId. Once that is done, silently retry fetching credentials. + /// If the fetch still fails, throw an error. + private func fetchBackupServiceAuth( + accountEntropyPool: SignalServiceKit.AccountEntropyPool, + accountIdentity: AccountIdentity + ) async throws -> BackupServiceAuth { + let backupKey = try MessageRootBackupKey(accountEntropyPool: accountEntropyPool, aci: accountIdentity.aci) + + func fetchBackupServiceAuth() async throws -> BackupServiceAuth { + return try await self.deps.backupRequestManager.fetchBackupServiceAuthForRegistration( + key: backupKey, + localAci: accountIdentity.aci, + chatServiceAuth: accountIdentity.chatServiceAuth + ) + } + + do { + return try await fetchBackupServiceAuth() + } catch SignalError.verificationFailed { + try await self.deps.backupIdService.updateMessageBackupIdForRegistration( + key: backupKey, + auth: accountIdentity.chatServiceAuth + ) + return try await fetchBackupServiceAuth() + } + } + @MainActor private func updateAccountAttributesAndFinish( accountIdentity: AccountIdentity, diff --git a/Signal/test/Registration/RegistrationCoordinatorTest.swift b/Signal/test/Registration/RegistrationCoordinatorTest.swift index 7ccb62f6cc..0af7601b66 100644 --- a/Signal/test/Registration/RegistrationCoordinatorTest.swift +++ b/Signal/test/Registration/RegistrationCoordinatorTest.swift @@ -108,6 +108,7 @@ public class RegistrationCoordinatorTest { accountEntropyPoolGenerator: { Stubs.accountEntropyPoolToGenerate }, accountKeyStore: accountKeyStore, backupArchiveManager: BackupArchiveManagerMock(), + backupIdService: MockBackupIdService(), backupNonceStore: BackupNonceMetadataStore(), backupRequestManager: BackupRequestManagerMock(), changeNumberPniManager: changeNumberPniManager, diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 9a8d710eff..7258a4962f 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -6905,7 +6905,7 @@ "REGISTER_RATE_LIMITING_ERROR" = "You have tried too often. Please wait a minute before trying again."; /* Body for a sheet warning users about a missing backup. */ -"REGISTRATION_BACKUP_RESTORE_ERROR_BACKUP_NOT_FOUND_BODY" = "The recovery key you entered is correct, but there is no backup associated with it. If you still have your old phone, make sure backups are enabled and that a backup has been completed and try again."; +"REGISTRATION_BACKUP_RESTORE_ERROR_BACKUP_NOT_FOUND_BODY" = "There is no backup associated with this recovery key. Please ensure the key is correct and try again."; /* Title for a sheet warning users about a missing backup. */ "REGISTRATION_BACKUP_RESTORE_ERROR_BACKUP_NOT_FOUND_TITLE" = "Backup Not Found"; diff --git a/SignalServiceKit/Backups/Settings/BackupIdService.swift b/SignalServiceKit/Backups/Settings/BackupIdService.swift index e744d1e51a..308a604d1c 100644 --- a/SignalServiceKit/Backups/Settings/BackupIdService.swift +++ b/SignalServiceKit/Backups/Settings/BackupIdService.swift @@ -17,6 +17,11 @@ public protocol BackupIdService { localAci: Aci, auth: ChatServiceAuth, ) async throws + + func updateMessageBackupIdForRegistration( + key: MessageRootBackupKey, + auth: ChatServiceAuth + ) async throws } // MARK: - @@ -101,23 +106,40 @@ final class BackupIdServiceImpl: BackupIdService { } } + func updateMessageBackupIdForRegistration( + key: MessageRootBackupKey, + auth: ChatServiceAuth + ) async throws { + guard FeatureFlags.Backups.supported else { + return + } + + try await registerBackupId( + localAci: key.aci, + messageBackupKey: key, + mediaBackupKey: nil, + auth: auth + ) + } + private func registerBackupId( localAci: Aci, messageBackupKey: MessageRootBackupKey, - mediaBackupKey: MediaRootBackupKey, + mediaBackupKey: MediaRootBackupKey?, auth: ChatServiceAuth ) async throws { let messageBackupRequestContext: BackupAuthCredentialRequestContext = .create( backupKey: messageBackupKey.serialize(), aci: localAci.rawUUID ) - let mediaBackupRequestContext: BackupAuthCredentialRequestContext = .create( - backupKey: mediaBackupKey.serialize(), - aci: localAci.rawUUID - ) - let base64MessageRequestContext = messageBackupRequestContext.getRequest().serialize().base64EncodedString() - let base64MediaRequestContext = mediaBackupRequestContext.getRequest().serialize().base64EncodedString() + let base64MediaRequestContext = mediaBackupKey.map { + let mediaBackupRequestContext: BackupAuthCredentialRequestContext = .create( + backupKey: $0.serialize(), + aci: localAci.rawUUID + ) + return mediaBackupRequestContext.getRequest().serialize().base64EncodedString() + } _ = try await networkManager.asyncRequest( .registerBackupId( @@ -134,16 +156,18 @@ final class BackupIdServiceImpl: BackupIdService { private extension TSRequest { static func registerBackupId( backupId: String, - mediaBackupId: String, + mediaBackupId: String?, auth: ChatServiceAuth ) -> TSRequest { + var parameters = [ "messagesBackupAuthCredentialRequest": backupId ] + if let mediaBackupId { + parameters["mediaBackupAuthCredentialRequest"] = mediaBackupId + } + var request = TSRequest( url: URL(string: "v1/archives/backupid")!, method: "PUT", - parameters: [ - "messagesBackupAuthCredentialRequest": backupId, - "mediaBackupAuthCredentialRequest": mediaBackupId - ] + parameters: parameters ) request.auth = .identified(auth) return request @@ -155,6 +179,10 @@ private extension TSRequest { #if TESTABLE_BUILD class MockBackupIdService: BackupIdService { + func updateMessageBackupIdForRegistration(key: MessageRootBackupKey, auth: ChatServiceAuth) async throws { + // Do nothing + } + func registerBackupIDIfNecessary(localAci: Aci, auth: ChatServiceAuth) async throws { // Do nothing }