Silently register backupId on restore failure

This commit is contained in:
Pete Walters 2025-10-14 17:40:14 -05:00 committed by GitHub
parent 989749cd64
commit 1d67bef04b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 91 additions and 31 deletions

View File

@ -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(

View File

@ -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,

View File

@ -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,

View File

@ -108,6 +108,7 @@ public class RegistrationCoordinatorTest {
accountEntropyPoolGenerator: { Stubs.accountEntropyPoolToGenerate },
accountKeyStore: accountKeyStore,
backupArchiveManager: BackupArchiveManagerMock(),
backupIdService: MockBackupIdService(),
backupNonceStore: BackupNonceMetadataStore(),
backupRequestManager: BackupRequestManagerMock(),
changeNumberPniManager: changeNumberPniManager,

View File

@ -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";

View File

@ -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
}