Only register the Backup public key once

This commit is contained in:
Sasha Weiss 2025-09-26 09:51:12 -07:00 committed by GitHub
parent a97cff62d5
commit 90c176502e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 60 additions and 167 deletions

View File

@ -1537,7 +1537,7 @@ private struct BackupExportProgressView: View {
private var progressBarState: ProgressBarState {
switch latestExportProgressUpdate.currentStep {
case .registerBackupId, .backupExport, .backupUpload:
case .backupExport, .backupUpload:
let percentExportCompleted = latestExportProgressUpdate.progress(for: .backupExport)?.percentComplete ?? 0
let percentUploadCompleted = latestExportProgressUpdate.progress(for: .backupUpload)?.percentComplete ?? 0
let percentComplete = (0.95 * percentExportCompleted) + (0.05 * percentUploadCompleted)

View File

@ -137,8 +137,10 @@ class InternalSettingsViewController: OWSTableViewController2 {
}
if mode != .registration {
backupsSection.add(.actionItem(withText: "Export + Validate Message Backup proto") {
self.exportMessageBackupProto()
backupsSection.add(.actionItem(withText: "Export + Validate Message Backup proto") { [self] in
Task {
await exportMessageBackupProto()
}
})
}
if FeatureFlags.Backups.showOptimizeMedia {
@ -368,152 +370,69 @@ private extension InternalSettingsViewController {
SpinningCheckmarks.shouldSpin = !wasSpinning
}
func exportMessageBackupProto() {
ModalActivityIndicatorViewController.present(
fromViewController: self,
canCancel: false
) { modal in
func dismissModalAndToast(_ message: String) {
DependenciesBridge.shared.backupArchiveErrorPresenter.presentOverTopmostViewController(completion: {
modal.dismiss {
self.presentToast(text: message)
}
})
}
func exportMessageBackupProtoFile() {
Task {
let result = await Result(catching: {
try await self.exportMessageBackupProtoFile(presentingFrom: modal)
})
await MainActor.run {
switch result {
case .success:
dismissModalAndToast("Success! Key copied to clipboard")
case .failure(let error):
dismissModalAndToast("Failed! \(error.localizedDescription)")
}
}
}
}
DispatchQueue.main.async {
let actionSheet = ActionSheetController(title: "Choose backup destination:")
let localFileAction = ActionSheetAction(title: "Local device") { _ in
exportMessageBackupProtoFile()
}
let remoteFileAction = ActionSheetAction(title: "Remote server") { _ in
Task {
let result = await Result(catching: {
try await self.exportMessageBackupProtoRemotely()
})
await MainActor.run {
switch result {
case .success:
dismissModalAndToast("Done")
case .failure(let error):
dismissModalAndToast("Failed! \(error.localizedDescription)")
}
}
}
}
actionSheet.addAction(localFileAction)
actionSheet.addAction(remoteFileAction)
modal.presentActionSheet(actionSheet)
}
}
}
// Right now this "local" backup uses the same format and encryption scheme
// as the remote backup. In the future, this should use the local backup
// format and encryption scheme.
func exportMessageBackupProtoFile(
presentingFrom vc: UIViewController
) async throws {
func exportMessageBackupProto() async {
let accountKeyStore = DependenciesBridge.shared.accountKeyStore
let backupArchiveManager = DependenciesBridge.shared.backupArchiveManager
let db = DependenciesBridge.shared.db
let errorPresenter = DependenciesBridge.shared.backupArchiveErrorPresenter
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
let (messageBackupKey, localIdentifiers) = try SSKEnvironment.shared.databaseStorageRef.read { tx in
let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx)!
return (
try accountKeyStore.getMessageRootBackupKey(aci: localIdentifiers.aci, tx: tx),
localIdentifiers
)
func presentBackupErrorsAndToast(_ message: String) {
errorPresenter.presentOverTopmostViewController { [self] in
presentToast(text: message)
}
}
guard let messageBackupKey else {
return
}
let backupKey: MessageBackupKey
let exportMetadata: Upload.EncryptedBackupUploadMetadata
do {
(backupKey, exportMetadata) = try await ModalActivityIndicatorViewController.presentAndPropagateResult(
from: self
) {
let (messageBackupKey, localIdentifiers) = try db.read { tx in
let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx)!
return (
try accountKeyStore.getMessageRootBackupKey(aci: localIdentifiers.aci, tx: tx)!,
localIdentifiers,
)
}
let backupEncryptionKey = try MessageBackupKey(
backupKey: messageBackupKey.backupKey,
backupId: messageBackupKey.backupId
)
let metadata = try await backupArchiveManager.exportEncryptedBackup(
localIdentifiers: localIdentifiers,
backupPurpose: .remoteExport(key: messageBackupKey, chatAuth: .implicit()),
progress: nil
)
let keyString = "AES key: \(backupEncryptionKey.aesKey.base64EncodedString())"
+ "\nHMAC key: \(backupEncryptionKey.hmacKey.base64EncodedString())"
await withCheckedContinuation { continuation in
DispatchQueue.main.async {
let activityVC = UIActivityViewController(
activityItems: [metadata.fileUrl],
applicationActivities: nil
let backupKey = try MessageBackupKey(
backupKey: messageBackupKey.backupKey,
backupId: messageBackupKey.backupId
)
activityVC.popoverPresentationController?.sourceView = self.view
activityVC.completionWithItemsHandler = { _, _, _, _ in
UIPasteboard.general.string = keyString
continuation.resume()
}
vc.present(activityVC, animated: true)
let exportMetadata = try await backupArchiveManager.exportEncryptedBackup(
localIdentifiers: localIdentifiers,
backupPurpose: .remoteExport(key: messageBackupKey, chatAuth: .implicit()),
progress: nil
)
return (backupKey, exportMetadata)
}
}
}
func exportMessageBackupProtoRemotely() async throws {
let accountKeyStore = DependenciesBridge.shared.accountKeyStore
let backupArchiveManager = DependenciesBridge.shared.backupArchiveManager
let backupKeyService = DependenciesBridge.shared.backupKeyService
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
let (messageBackupKey, localIdentifiers) = try SSKEnvironment.shared.databaseStorageRef.read { tx in
let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx)!
return (
try accountKeyStore.getMessageRootBackupKey(aci: localIdentifiers.aci, tx: tx),
localIdentifiers
)
}
guard let messageBackupKey else {
} catch {
owsFailDebug("Failed to export Backup proto! \(error)")
presentBackupErrorsAndToast("Failed to export Backup proto!")
return
}
let metadata = try await backupArchiveManager.exportEncryptedBackup(
localIdentifiers: localIdentifiers,
backupPurpose: .remoteExport(key: messageBackupKey, chatAuth: .implicit()),
progress: nil
)
let keyString = "AES key: \(backupKey.aesKey.base64EncodedString())"
+ "\nHMAC key: \(backupKey.hmacKey.base64EncodedString())"
let registeredBackupKeyToken = try await backupKeyService.registerBackupKey(
localIdentifiers: localIdentifiers,
auth: .implicit()
let activityVC = UIActivityViewController(
activityItems: [exportMetadata.fileUrl],
applicationActivities: nil
)
activityVC.popoverPresentationController?.sourceView = view
activityVC.completionWithItemsHandler = { _, _, _, _ in
UIPasteboard.general.setItems(
[[UIPasteboard.typeAutomatic: keyString]],
options: [.expirationDate: Date().addingTimeInterval(120)]
)
_ = try await backupArchiveManager.uploadEncryptedBackup(
backupKey: messageBackupKey,
metadata: metadata,
registeredBackupKeyToken: registeredBackupKeyToken,
auth: .implicit(),
progress: nil,
)
presentBackupErrorsAndToast("Success! Encryption key copied.")
}
present(activityVC, animated: true)
}
}

View File

@ -42,7 +42,6 @@ public protocol BackupArchiveManager {
func uploadEncryptedBackup(
backupKey: MessageRootBackupKey,
metadata: Upload.EncryptedBackupUploadMetadata,
registeredBackupKeyToken: RegisteredBackupKeyToken,
auth: ChatServiceAuth,
progress: OWSProgressSink?,
) async throws -> Upload.Result<Upload.EncryptedBackupUploadMetadata>

View File

@ -188,7 +188,6 @@ public class BackupArchiveManagerImpl: BackupArchiveManager {
public func uploadEncryptedBackup(
backupKey: MessageRootBackupKey,
metadata: Upload.EncryptedBackupUploadMetadata,
registeredBackupKeyToken: RegisteredBackupKeyToken,
auth: ChatServiceAuth,
progress: OWSProgressSink?
) async throws -> Upload.Result<Upload.EncryptedBackupUploadMetadata> {

View File

@ -30,7 +30,6 @@ open class BackupArchiveManagerMock: BackupArchiveManager {
public func uploadEncryptedBackup(
backupKey: MessageRootBackupKey,
metadata: Upload.EncryptedBackupUploadMetadata,
registeredBackupKeyToken: RegisteredBackupKeyToken,
auth: ChatServiceAuth,
progress: OWSProgressSink?,
) async throws -> Upload.Result<Upload.EncryptedBackupUploadMetadata> {

View File

@ -6,7 +6,6 @@
import LibSignalClient
public enum BackupExportJobStep: String, OWSSequentialProgressStep {
case registerBackupId
case backupExport
case backupUpload
case listMedia
@ -18,11 +17,10 @@ public enum BackupExportJobStep: String, OWSSequentialProgressStep {
/// a given step should take.
public var progressUnitCount: UInt64 {
switch self {
case .registerBackupId: 1
case .backupExport: 40
case .backupUpload: 10
case .listMedia: 5
case .attachmentOrphaning: 2
case .attachmentOrphaning: 3
case .attachmentUpload: 40
case .offloading: 2
}
@ -245,18 +243,6 @@ class BackupExportJobImpl: BackupExportJob {
}
do {
logger.info("Starting...")
let registeredBackupKeyToken = try await withEstimatedProgressUpdates(
estimatedTimeToCompletion: 0.5,
progress: progress?.child(for: .registerBackupId).addSource(withLabel: "", unitCount: 1),
) { [backupKeyService] in
try await backupKeyService.registerBackupKey(
localIdentifiers: localIdentifiers,
auth: .implicit()
)
}
logger.info("Exporting backup...")
let uploadMetadata = try await backupArchiveManager.exportEncryptedBackup(
@ -298,7 +284,6 @@ class BackupExportJobImpl: BackupExportJob {
_ = try await backupArchiveManager.uploadEncryptedBackup(
backupKey: backupKey,
metadata: uploadMetadata,
registeredBackupKeyToken: registeredBackupKeyToken,
auth: .implicit(),
progress: progress?.child(for: .backupUpload),
)

View File

@ -5,11 +5,6 @@
import LibSignalClient
/// An opaque token returned after registering a BackupKey, which can be
/// required by APIs that require a BackupKey to have been previously registered
/// in order to succeed.
public struct RegisteredBackupKeyToken {}
/// Responsible for CRUD of the "BackupKey", which is an asymmetric key used to
/// sign Backup auth credentials.
///
@ -23,7 +18,7 @@ public protocol BackupKeyService {
func registerBackupKey(
localIdentifiers: LocalIdentifiers,
auth: ChatServiceAuth
) async throws -> RegisteredBackupKeyToken
) async throws
/// De-initialize Backups by deleting a previously-registered BackupKey.
/// This is effectively a "delete Backup" operation, as subsequent to this
@ -88,7 +83,7 @@ final class BackupKeyServiceImpl: BackupKeyService {
func registerBackupKey(
localIdentifiers: LocalIdentifiers,
auth: ChatServiceAuth
) async throws -> RegisteredBackupKeyToken {
) async throws {
try await _registerBackupKey(
localIdentifiers: localIdentifiers,
auth: auth,
@ -100,7 +95,7 @@ final class BackupKeyServiceImpl: BackupKeyService {
localIdentifiers: LocalIdentifiers,
auth: ChatServiceAuth,
retryOnFail: Bool
) async throws -> RegisteredBackupKeyToken {
) async throws {
let (messageBackupKey, mediaBackupKey) = try await db.awaitableWrite { tx in
try rootBackupKeys(localIdentifiers: localIdentifiers, tx: tx)
}
@ -125,9 +120,6 @@ final class BackupKeyServiceImpl: BackupKeyService {
_ = try await networkManager.asyncRequest(
.backupSetPublicKeyRequest(backupAuth: mediaBackupAuth)
)
return RegisteredBackupKeyToken()
} catch SignalError.verificationFailed where retryOnFail {
// This error is thrown if the backupID was never registered remotely.
// We *should* set it above in registerBackupIDIfNecessary based on local state,
@ -258,8 +250,8 @@ private extension TSRequest {
#if TESTABLE_BUILD
class MockBackupKeyService: BackupKeyService {
func registerBackupKey(localIdentifiers: LocalIdentifiers, auth: ChatServiceAuth) async throws -> RegisteredBackupKeyToken {
return RegisteredBackupKeyToken()
func registerBackupKey(localIdentifiers: LocalIdentifiers, auth: ChatServiceAuth) async throws {
// Do nothing
}
var deleteBackupKeyMock: (() async throws -> Void)?