1564 lines
69 KiB
Swift
1564 lines
69 KiB
Swift
//
|
||
// Copyright 2023 Signal Messenger, LLC
|
||
// SPDX-License-Identifier: AGPL-3.0-only
|
||
//
|
||
|
||
import GRDB
|
||
public import LibSignalClient
|
||
|
||
public enum BackupValidationError: Error {
|
||
case unknownFields([String])
|
||
case validationFailed(message: String, unknownFields: [String])
|
||
case ioError(String)
|
||
case unknownError
|
||
}
|
||
|
||
public enum BackupImportError: Error {
|
||
case unsupportedVersion
|
||
}
|
||
|
||
public class BackupArchiveManagerImpl: BackupArchiveManager {
|
||
public enum Constants {
|
||
fileprivate static let keyValueStoreCollectionName = "MessageBackupManager"
|
||
fileprivate static let keyValueStoreRestoreStateKey = "keyValueStoreRestoreStateKey"
|
||
fileprivate static let keyValueStoreNeedForwardSecrecyTokenFetchKey = "keyValueStoreNeedForwardSecrecyTokenFetchKey"
|
||
|
||
public static let supportedBackupVersion: UInt64 = 1
|
||
|
||
/// The ratio of frames processed for which to sample memory.
|
||
fileprivate static let memorySamplerFrameRatio: Float = BuildFlags.Backups.detailedBenchLogging ? 0.001 : 0
|
||
}
|
||
|
||
private class NotImplementedError: Error {}
|
||
private class BackupError: Error {}
|
||
private typealias LoggableErrorAndProto = BackupArchive.LoggableErrorAndProto
|
||
|
||
private let accountDataArchiver: BackupArchiveAccountDataArchiver
|
||
private let adHocCallArchiver: BackupArchiveAdHocCallArchiver
|
||
private let appVersion: AppVersion
|
||
private let attachmentDownloadManager: AttachmentDownloadManager
|
||
private let attachmentUploadManager: AttachmentUploadManager
|
||
private let avatarFetcher: BackupArchiveAvatarFetcher
|
||
private let backupArchiveErrorPresenter: BackupArchiveErrorPresenter
|
||
private let backupAttachmentCoordinator: BackupAttachmentCoordinator
|
||
private let backupAttachmentUploadEraStore: BackupAttachmentUploadEraStore
|
||
private let backupNonceMetadataStore: BackupNonceMetadataStore
|
||
private let backupRequestManager: BackupRequestManager
|
||
private let backupSettingsStore: BackupSettingsStore
|
||
private let backupStickerPackDownloadStore: BackupStickerPackDownloadStore
|
||
private let callLinkRecipientArchiver: BackupArchiveCallLinkRecipientArchiver
|
||
private let chatArchiver: BackupArchiveChatArchiver
|
||
private let chatItemArchiver: BackupArchiveChatItemArchiver
|
||
private let contactRecipientArchiver: BackupArchiveContactRecipientArchiver
|
||
private let databaseChangeObserver: DatabaseChangeObserver
|
||
private let dateProvider: DateProvider
|
||
private let dateProviderMonotonic: DateProviderMonotonic
|
||
private let db: any DB
|
||
private let disappearingMessagesExpirationJob: DisappearingMessagesExpirationJob
|
||
private let distributionListRecipientArchiver: BackupArchiveDistributionListRecipientArchiver
|
||
private let encryptedStreamProvider: BackupArchiveEncryptedProtoStreamProvider
|
||
private let fullTextSearchIndexer: BackupArchiveFullTextSearchIndexer
|
||
private let groupRecipientArchiver: BackupArchiveGroupRecipientArchiver
|
||
private let kvStore: KeyValueStore
|
||
private let libsignalNet: LibSignalClient.Net
|
||
private let localStorage: AccountKeyStore
|
||
private let localRecipientArchiver: BackupArchiveLocalRecipientArchiver
|
||
private let logger: PrefixedLogger
|
||
private let messagePipelineSupervisor: MessagePipelineSupervisor
|
||
private let oversizeTextArchiver: BackupArchiveInlinedOversizeTextArchiver
|
||
private let plaintextStreamProvider: BackupArchivePlaintextProtoStreamProvider
|
||
private let postFrameRestoreActionManager: BackupArchivePostFrameRestoreActionManager
|
||
private let releaseNotesRecipientArchiver: BackupArchiveReleaseNotesRecipientArchiver
|
||
private let remoteConfigManager: RemoteConfigManager
|
||
private let stickerPackArchiver: BackupArchiveStickerPackArchiver
|
||
private let tsAccountManager: TSAccountManager
|
||
|
||
init(
|
||
accountDataArchiver: BackupArchiveAccountDataArchiver,
|
||
adHocCallArchiver: BackupArchiveAdHocCallArchiver,
|
||
appVersion: AppVersion,
|
||
attachmentDownloadManager: AttachmentDownloadManager,
|
||
attachmentUploadManager: AttachmentUploadManager,
|
||
avatarFetcher: BackupArchiveAvatarFetcher,
|
||
backupArchiveErrorPresenter: BackupArchiveErrorPresenter,
|
||
backupAttachmentCoordinator: BackupAttachmentCoordinator,
|
||
backupAttachmentUploadEraStore: BackupAttachmentUploadEraStore,
|
||
backupNonceMetadataStore: BackupNonceMetadataStore,
|
||
backupRequestManager: BackupRequestManager,
|
||
backupSettingsStore: BackupSettingsStore,
|
||
backupStickerPackDownloadStore: BackupStickerPackDownloadStore,
|
||
callLinkRecipientArchiver: BackupArchiveCallLinkRecipientArchiver,
|
||
chatArchiver: BackupArchiveChatArchiver,
|
||
chatItemArchiver: BackupArchiveChatItemArchiver,
|
||
contactRecipientArchiver: BackupArchiveContactRecipientArchiver,
|
||
databaseChangeObserver: DatabaseChangeObserver,
|
||
dateProvider: @escaping DateProvider,
|
||
dateProviderMonotonic: @escaping DateProviderMonotonic,
|
||
db: any DB,
|
||
disappearingMessagesExpirationJob: DisappearingMessagesExpirationJob,
|
||
distributionListRecipientArchiver: BackupArchiveDistributionListRecipientArchiver,
|
||
encryptedStreamProvider: BackupArchiveEncryptedProtoStreamProvider,
|
||
fullTextSearchIndexer: BackupArchiveFullTextSearchIndexer,
|
||
groupRecipientArchiver: BackupArchiveGroupRecipientArchiver,
|
||
libsignalNet: LibSignalClient.Net,
|
||
localStorage: AccountKeyStore,
|
||
localRecipientArchiver: BackupArchiveLocalRecipientArchiver,
|
||
messagePipelineSupervisor: MessagePipelineSupervisor,
|
||
oversizeTextArchiver: BackupArchiveInlinedOversizeTextArchiver,
|
||
plaintextStreamProvider: BackupArchivePlaintextProtoStreamProvider,
|
||
postFrameRestoreActionManager: BackupArchivePostFrameRestoreActionManager,
|
||
releaseNotesRecipientArchiver: BackupArchiveReleaseNotesRecipientArchiver,
|
||
remoteConfigManager: RemoteConfigManager,
|
||
stickerPackArchiver: BackupArchiveStickerPackArchiver,
|
||
tsAccountManager: TSAccountManager,
|
||
) {
|
||
self.accountDataArchiver = accountDataArchiver
|
||
self.appVersion = appVersion
|
||
self.attachmentDownloadManager = attachmentDownloadManager
|
||
self.attachmentUploadManager = attachmentUploadManager
|
||
self.avatarFetcher = avatarFetcher
|
||
self.backupArchiveErrorPresenter = backupArchiveErrorPresenter
|
||
self.backupAttachmentCoordinator = backupAttachmentCoordinator
|
||
self.backupAttachmentUploadEraStore = backupAttachmentUploadEraStore
|
||
self.backupNonceMetadataStore = backupNonceMetadataStore
|
||
self.backupRequestManager = backupRequestManager
|
||
self.backupSettingsStore = backupSettingsStore
|
||
self.backupStickerPackDownloadStore = backupStickerPackDownloadStore
|
||
self.callLinkRecipientArchiver = callLinkRecipientArchiver
|
||
self.chatArchiver = chatArchiver
|
||
self.chatItemArchiver = chatItemArchiver
|
||
self.contactRecipientArchiver = contactRecipientArchiver
|
||
self.databaseChangeObserver = databaseChangeObserver
|
||
self.dateProvider = dateProvider
|
||
self.dateProviderMonotonic = dateProviderMonotonic
|
||
self.db = db
|
||
self.disappearingMessagesExpirationJob = disappearingMessagesExpirationJob
|
||
self.distributionListRecipientArchiver = distributionListRecipientArchiver
|
||
self.encryptedStreamProvider = encryptedStreamProvider
|
||
self.fullTextSearchIndexer = fullTextSearchIndexer
|
||
self.groupRecipientArchiver = groupRecipientArchiver
|
||
self.kvStore = KeyValueStore(collection: Constants.keyValueStoreCollectionName)
|
||
self.libsignalNet = libsignalNet
|
||
self.localStorage = localStorage
|
||
self.localRecipientArchiver = localRecipientArchiver
|
||
self.logger = PrefixedLogger(prefix: "[Backups]")
|
||
self.messagePipelineSupervisor = messagePipelineSupervisor
|
||
self.oversizeTextArchiver = oversizeTextArchiver
|
||
self.plaintextStreamProvider = plaintextStreamProvider
|
||
self.postFrameRestoreActionManager = postFrameRestoreActionManager
|
||
self.releaseNotesRecipientArchiver = releaseNotesRecipientArchiver
|
||
self.remoteConfigManager = remoteConfigManager
|
||
self.stickerPackArchiver = stickerPackArchiver
|
||
self.adHocCallArchiver = adHocCallArchiver
|
||
self.tsAccountManager = tsAccountManager
|
||
}
|
||
|
||
// MARK: - Remote backups
|
||
|
||
public func downloadEncryptedBackup(
|
||
backupKey: MessageRootBackupKey,
|
||
backupAuth: BackupServiceAuth,
|
||
progress: OWSProgressSink?,
|
||
logger: PrefixedLogger,
|
||
) async throws -> URL {
|
||
let metadata = try await backupRequestManager.fetchBackupRequestMetadata(auth: backupAuth, logger: logger)
|
||
let tmpFileUrl = try await attachmentDownloadManager.downloadBackup(
|
||
metadata: metadata,
|
||
progress: progress,
|
||
)
|
||
|
||
// Once protos calm down, this can be enabled to warn/error on failed validation
|
||
// try await validateBackup(localIdentifiers: localIdentifiers, fileUrl: tmpFileUrl)
|
||
|
||
return tmpFileUrl
|
||
}
|
||
|
||
public func backupCdnInfo(
|
||
backupKey: MessageRootBackupKey,
|
||
backupAuth: BackupServiceAuth,
|
||
logger: PrefixedLogger,
|
||
) async throws -> BackupCdnInfo {
|
||
let metadata = try await backupRequestManager.fetchBackupRequestMetadata(auth: backupAuth, logger: logger)
|
||
return try await attachmentDownloadManager.backupCdnInfo(metadata: metadata)
|
||
}
|
||
|
||
public func uploadEncryptedBackup(
|
||
backupKey: MessageRootBackupKey,
|
||
metadata: Upload.EncryptedBackupUploadMetadata,
|
||
auth: ChatServiceAuth,
|
||
progress: OWSProgressSink?,
|
||
logger: PrefixedLogger,
|
||
) async throws -> Upload.Result<Upload.EncryptedBackupUploadMetadata> {
|
||
try Task.checkCancellation()
|
||
guard db.read(block: { tsAccountManager.registrationState(tx: $0).isPrimaryDevice }) == true else {
|
||
throw OWSAssertionError("Backing up not on a registered primary!")
|
||
}
|
||
|
||
let backupAuth = try await backupRequestManager.fetchBackupServiceAuth(
|
||
for: backupKey,
|
||
localAci: backupKey.aci,
|
||
auth: auth,
|
||
logger: logger,
|
||
)
|
||
let form: Upload.Form
|
||
do {
|
||
form = try await backupRequestManager.fetchBackupUploadForm(
|
||
backupByteLength: metadata.encryptedDataLength,
|
||
auth: backupAuth,
|
||
logger: logger,
|
||
)
|
||
} catch let error {
|
||
switch error as? BackupArchive.Response.BackupUploadFormError {
|
||
case .tooLarge:
|
||
logger.warn("Backup too large! \(metadata.encryptedDataLength)")
|
||
default:
|
||
break
|
||
}
|
||
throw error
|
||
}
|
||
let result = try await attachmentUploadManager.uploadBackup(
|
||
localUploadMetadata: metadata,
|
||
form: form,
|
||
progress: progress,
|
||
)
|
||
|
||
await db.awaitableWrite { tx in
|
||
let backupFileSizeBytes: UInt64
|
||
let backupMediaSizeBytes: UInt64
|
||
switch backupSettingsStore.backupPlan(tx: tx) {
|
||
case .paid, .paidExpiringSoon, .paidAsTester:
|
||
backupFileSizeBytes = UInt64(metadata.encryptedDataLength)
|
||
backupMediaSizeBytes = metadata.attachmentByteSize
|
||
case .free:
|
||
backupFileSizeBytes = UInt64(metadata.encryptedDataLength)
|
||
backupMediaSizeBytes = 0
|
||
case .disabled, .disabling:
|
||
owsFailDebug("Shouldn't generate backup when backups is disabled", logger: logger)
|
||
backupFileSizeBytes = 0
|
||
backupMediaSizeBytes = 0
|
||
}
|
||
|
||
backupSettingsStore.setLastBackupDetails(
|
||
date: metadata.exportStartDate,
|
||
backupFileSizeBytes: backupFileSizeBytes,
|
||
backupMediaSizeBytes: backupMediaSizeBytes,
|
||
tx: tx,
|
||
)
|
||
|
||
if let nonceMetadata = metadata.nonceMetadata {
|
||
backupNonceMetadataStore.setLastForwardSecrecyToken(
|
||
nonceMetadata.forwardSecrecyToken,
|
||
for: backupKey,
|
||
tx: tx,
|
||
)
|
||
backupNonceMetadataStore.setNextSecretMetadata(
|
||
nonceMetadata.nextSecretMetadata,
|
||
for: backupKey,
|
||
tx: tx,
|
||
)
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// MARK: - Export
|
||
|
||
public func exportEncryptedBackup(
|
||
localIdentifiers: LocalIdentifiers,
|
||
backupPurpose: BackupExportPurpose,
|
||
progress progressSink: OWSProgressSink?,
|
||
logger: PrefixedLogger,
|
||
) async throws -> Upload.EncryptedBackupUploadMetadata {
|
||
let attachmentByteCounter = BackupArchiveAttachmentByteCounter()
|
||
let startDate = dateProvider()
|
||
|
||
// Filter included content according to the purpose of this backup.
|
||
let includedContentFilter = BackupArchive.IncludedContentFilter(
|
||
backupPurpose: backupPurpose.libsignalPurpose,
|
||
)
|
||
|
||
switch backupPurpose {
|
||
case .remoteExport(let key, let chatAuth):
|
||
// If an SVRB restore has been scheduled, do this restore before continuing
|
||
// with the remote backup. This ensures the local and remote state are
|
||
// consistent and avoids the possibility of a backup being created that
|
||
// can't be recovered using the material in SVRB.
|
||
if db.read(block: { needsRestoreFromSVRBBeforeRemoteExport(tx: $0) }) {
|
||
do {
|
||
try await fetchRemoteSVRBForwardSecrecyToken(key: key, auth: chatAuth, logger: logger)
|
||
} catch SVRBError.unrecoverable {
|
||
// Not found, so consider a success and fallthrough
|
||
logger.info("SVRB not found, skipping restore.")
|
||
} catch {
|
||
logger.warn("Encountered error restoring SVRB: \(error)")
|
||
throw error
|
||
}
|
||
|
||
await db.awaitableWrite {
|
||
kvStore.setBool(
|
||
false,
|
||
key: Constants.keyValueStoreNeedForwardSecrecyTokenFetchKey,
|
||
transaction: $0,
|
||
)
|
||
}
|
||
}
|
||
case .linkNsync:
|
||
break
|
||
}
|
||
|
||
let encryptionMetadata = try await backupPurpose.deriveEncryptionMetadataWithSVRBIfNeeded(
|
||
backupRequestManager: backupRequestManager,
|
||
db: db,
|
||
libsignalNet: libsignalNet,
|
||
nonceStore: backupNonceMetadataStore,
|
||
)
|
||
|
||
let metadata = try await _exportBackup(
|
||
localIdentifiers: localIdentifiers,
|
||
backupPurpose: backupPurpose.libsignalPurpose,
|
||
startDate: startDate,
|
||
includedContentFilter: includedContentFilter,
|
||
progressSink: progressSink,
|
||
attachmentByteCounter: attachmentByteCounter,
|
||
benchTitle: "Export encrypted Backup",
|
||
openOutputStreamBlock: { exportProgress, tx in
|
||
return encryptedStreamProvider.openEncryptedOutputFileStream(
|
||
startDate: startDate,
|
||
encryptionMetadata: encryptionMetadata,
|
||
exportProgress: exportProgress,
|
||
attachmentByteCounter: attachmentByteCounter,
|
||
tx: tx,
|
||
)
|
||
},
|
||
)
|
||
|
||
try await self.validateEncryptedBackup(
|
||
fileUrl: metadata.fileUrl,
|
||
backupEncryptionKey: encryptionMetadata.encryptionKey,
|
||
backupPurpose: backupPurpose.libsignalPurpose,
|
||
)
|
||
|
||
return metadata
|
||
}
|
||
|
||
#if TESTABLE_BUILD
|
||
public func exportPlaintextBackupForTests(
|
||
localIdentifiers: LocalIdentifiers,
|
||
) async throws -> URL {
|
||
let attachmentByteCounter = BackupArchiveAttachmentByteCounter()
|
||
let startDate = dateProvider()
|
||
|
||
// For the integration tests, don't filter out any content. The premise
|
||
// of the tests is to verify that round-tripping a Backup file is
|
||
// idempotent. The device transfer purpose includes everything.
|
||
let includedContentFilter = BackupArchive.IncludedContentFilter(
|
||
backupPurpose: .deviceTransfer,
|
||
)
|
||
|
||
return try await _exportBackup(
|
||
localIdentifiers: localIdentifiers,
|
||
backupPurpose: .remoteBackup,
|
||
startDate: startDate,
|
||
includedContentFilter: includedContentFilter,
|
||
progressSink: nil,
|
||
attachmentByteCounter: attachmentByteCounter,
|
||
benchTitle: "Export plaintext Backup",
|
||
openOutputStreamBlock: { exportProgress, tx in
|
||
return plaintextStreamProvider.openPlaintextOutputFileStream(
|
||
exportProgress: exportProgress,
|
||
)
|
||
},
|
||
)
|
||
}
|
||
#endif
|
||
|
||
private func _exportBackup<OutputStreamMetadata>(
|
||
localIdentifiers: LocalIdentifiers,
|
||
backupPurpose: MessageBackupPurpose,
|
||
startDate: Date,
|
||
includedContentFilter: BackupArchive.IncludedContentFilter,
|
||
progressSink: OWSProgressSink?,
|
||
attachmentByteCounter: BackupArchiveAttachmentByteCounter,
|
||
benchTitle: String,
|
||
openOutputStreamBlock: (
|
||
BackupArchiveExportProgress?,
|
||
DBReadTransaction,
|
||
) -> BackupArchive.ProtoStream.OpenOutputStreamResult<OutputStreamMetadata>,
|
||
) async throws -> OutputStreamMetadata {
|
||
let prepareOversizeTextAttachmentsProgressSink: OWSProgressSink?
|
||
let exportProgress: BackupArchiveExportProgress?
|
||
if let progressSink {
|
||
prepareOversizeTextAttachmentsProgressSink = await progressSink.addChild(
|
||
withLabel: "Export Backup: Oversize Text Attachments",
|
||
unitCount: 5,
|
||
)
|
||
exportProgress = try await .prepare(
|
||
sink: await progressSink.addChild(
|
||
withLabel: "Export Backup: Export Frames",
|
||
unitCount: 95,
|
||
),
|
||
db: db,
|
||
)
|
||
} else {
|
||
prepareOversizeTextAttachmentsProgressSink = nil
|
||
exportProgress = nil
|
||
}
|
||
|
||
try await oversizeTextArchiver.populateTableIncrementally(progress: prepareOversizeTextAttachmentsProgressSink)
|
||
|
||
// Before we export, we need to make sure we have an MRBK – the export
|
||
// will refetch this, and throw if it's missing.
|
||
_ = await db.awaitableWrite { tx in
|
||
localStorage.getOrGenerateMediaRootBackupKey(tx: tx)
|
||
}
|
||
|
||
return try db.read { tx in
|
||
let outputStreamMetadata = try BenchMemory(
|
||
title: benchTitle,
|
||
memorySamplerRatio: Constants.memorySamplerFrameRatio,
|
||
logInProduction: true,
|
||
) { memorySampler -> OutputStreamMetadata in
|
||
let outputStream: BackupArchiveProtoOutputStream
|
||
let outputStreamMetadataProvider: () throws -> OutputStreamMetadata
|
||
switch openOutputStreamBlock(exportProgress, tx) {
|
||
case .success(let _outputStream, let _outputStreamMetadataProvider):
|
||
outputStream = _outputStream
|
||
outputStreamMetadataProvider = _outputStreamMetadataProvider
|
||
case .unableToOpenFileStream:
|
||
throw OWSAssertionError("Unable to open output file stream!")
|
||
}
|
||
|
||
try self._exportBackup(
|
||
outputStream: outputStream,
|
||
localIdentifiers: localIdentifiers,
|
||
backupPurpose: backupPurpose,
|
||
startDate: startDate,
|
||
attachmentByteCounter: attachmentByteCounter,
|
||
includedContentFilter: includedContentFilter,
|
||
currentAppVersion: appVersion.currentAppVersion,
|
||
firstAppVersion: appVersion.firstBackupAppVersion ?? appVersion.firstAppVersion,
|
||
memorySampler: memorySampler,
|
||
tx: tx,
|
||
)
|
||
|
||
return try outputStreamMetadataProvider()
|
||
}
|
||
|
||
return outputStreamMetadata
|
||
}
|
||
}
|
||
|
||
private func _exportBackup(
|
||
outputStream stream: BackupArchiveProtoOutputStream,
|
||
localIdentifiers: LocalIdentifiers,
|
||
backupPurpose: MessageBackupPurpose,
|
||
startDate: Date,
|
||
attachmentByteCounter: BackupArchiveAttachmentByteCounter,
|
||
includedContentFilter: BackupArchive.IncludedContentFilter,
|
||
currentAppVersion: String,
|
||
firstAppVersion: String,
|
||
memorySampler: MemorySampler,
|
||
tx: DBReadTransaction,
|
||
) throws {
|
||
let bencher = BackupArchive.ArchiveBencher(
|
||
dateProviderMonotonic: dateProviderMonotonic,
|
||
memorySampler: memorySampler,
|
||
)
|
||
let remoteConfig = remoteConfigManager.currentConfig()
|
||
let currentUploadEra = backupAttachmentUploadEraStore.currentUploadEra(tx: tx)
|
||
let backupVersion = Constants.supportedBackupVersion
|
||
let purposeString: String = switch backupPurpose {
|
||
case .deviceTransfer: "LinkNSync"
|
||
case .remoteBackup: "RemoteBackup"
|
||
}
|
||
|
||
// We already have a passed-in MRBK, but that came from outside this read tx so
|
||
// refetch it to make sure. If it changed to a new value, use the new value, thats fine
|
||
// (though unexpected). If it changed to _nil_ (should never happen on primaries), exit.
|
||
guard let mediaRootBackupKey = localStorage.getMediaRootBackupKey(tx: tx) else {
|
||
throw OWSAssertionError("MRBK unset as backup being created!")
|
||
}
|
||
|
||
var errors = [LoggableErrorAndProto]()
|
||
let result = Result<Void, Error>(catching: {
|
||
logger.info("Exporting for \(purposeString) with version \(backupVersion), timestamp \(startDate.ows_millisecondsSince1970)")
|
||
|
||
try autoreleasepool {
|
||
try writeHeader(
|
||
stream: stream,
|
||
backupVersion: backupVersion,
|
||
startDate: startDate,
|
||
currentAppVersion: currentAppVersion,
|
||
firstAppVersion: firstAppVersion,
|
||
mediaRootBackupKey: mediaRootBackupKey,
|
||
tx: tx,
|
||
)
|
||
}
|
||
try Task.checkCancellation()
|
||
|
||
let customChatColorContext = BackupArchive.CustomChatColorArchivingContext(
|
||
localIdentifiers: localIdentifiers,
|
||
startDate: startDate,
|
||
remoteConfig: remoteConfig,
|
||
currentUploadEra: currentUploadEra,
|
||
bencher: bencher,
|
||
attachmentByteCounter: attachmentByteCounter,
|
||
includedContentFilter: includedContentFilter,
|
||
tx: tx,
|
||
)
|
||
try autoreleasepool {
|
||
let accountDataResult = accountDataArchiver.archiveAccountData(
|
||
stream: stream,
|
||
context: customChatColorContext,
|
||
)
|
||
switch accountDataResult {
|
||
case .success:
|
||
break
|
||
case .failure(let error):
|
||
errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
|
||
throw OWSAssertionError("Failed to archive account data")
|
||
}
|
||
}
|
||
try Task.checkCancellation()
|
||
|
||
let localRecipientResult = localRecipientArchiver.archiveLocalRecipient(
|
||
stream: stream,
|
||
bencher: bencher,
|
||
localIdentifiers: localIdentifiers,
|
||
tx: tx,
|
||
)
|
||
|
||
try Task.checkCancellation()
|
||
|
||
let localRecipientId: BackupArchive.RecipientId
|
||
switch localRecipientResult {
|
||
case .success(let success):
|
||
localRecipientId = success
|
||
case .failure(let error):
|
||
errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
|
||
throw OWSAssertionError("Failed to archive local recipient!")
|
||
}
|
||
|
||
guard
|
||
let localSignalRecipientRowId = localRecipientArchiver.fetchLocalRecipientRowId(
|
||
localIdentifiers: localIdentifiers,
|
||
tx: tx,
|
||
)
|
||
else {
|
||
throw OWSAssertionError("Failed to fetch local recipient row ID!")
|
||
}
|
||
|
||
let recipientArchivingContext = BackupArchive.RecipientArchivingContext(
|
||
localRecipientId: localRecipientId,
|
||
localSignalRecipientRowId: localSignalRecipientRowId,
|
||
localIdentifiers: localIdentifiers,
|
||
startDate: startDate,
|
||
remoteConfig: remoteConfig,
|
||
currentUploadEra: currentUploadEra,
|
||
bencher: bencher,
|
||
attachmentByteCounter: attachmentByteCounter,
|
||
includedContentFilter: includedContentFilter,
|
||
tx: tx,
|
||
)
|
||
|
||
try autoreleasepool {
|
||
switch releaseNotesRecipientArchiver.archiveReleaseNotesRecipient(
|
||
stream: stream,
|
||
context: recipientArchivingContext,
|
||
) {
|
||
case .success:
|
||
break
|
||
case .failure(let error):
|
||
errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
|
||
throw OWSAssertionError("Failed to archive release notes channel!")
|
||
}
|
||
}
|
||
try Task.checkCancellation()
|
||
|
||
switch try contactRecipientArchiver.archiveAllContactRecipients(
|
||
stream: stream,
|
||
context: recipientArchivingContext,
|
||
) {
|
||
case .success:
|
||
break
|
||
case .partialSuccess(let partialFailures):
|
||
errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false) })
|
||
case .completeFailure(let error):
|
||
errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
|
||
throw BackupError()
|
||
}
|
||
|
||
switch try groupRecipientArchiver.archiveAllGroupRecipients(
|
||
stream: stream,
|
||
context: recipientArchivingContext,
|
||
) {
|
||
case .success:
|
||
break
|
||
case .partialSuccess(let partialFailures):
|
||
errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false) })
|
||
case .completeFailure(let error):
|
||
errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
|
||
throw BackupError()
|
||
}
|
||
|
||
switch try distributionListRecipientArchiver.archiveAllDistributionListRecipients(
|
||
stream: stream,
|
||
context: recipientArchivingContext,
|
||
) {
|
||
case .success:
|
||
break
|
||
case .partialSuccess(let partialFailures):
|
||
errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false) })
|
||
case .completeFailure(let error):
|
||
errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
|
||
throw BackupError()
|
||
}
|
||
|
||
switch try callLinkRecipientArchiver.archiveAllCallLinkRecipients(
|
||
stream: stream,
|
||
context: recipientArchivingContext,
|
||
) {
|
||
case .success:
|
||
break
|
||
case .partialSuccess(let partialFailures):
|
||
errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false) })
|
||
case .completeFailure(let error):
|
||
errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
|
||
throw BackupError()
|
||
}
|
||
|
||
let chatArchivingContext = BackupArchive.ChatArchivingContext(
|
||
customChatColorContext: customChatColorContext,
|
||
recipientContext: recipientArchivingContext,
|
||
localIdentifiers: localIdentifiers,
|
||
startDate: startDate,
|
||
remoteConfig: remoteConfig,
|
||
currentUploadEra: currentUploadEra,
|
||
bencher: bencher,
|
||
attachmentByteCounter: attachmentByteCounter,
|
||
includedContentFilter: includedContentFilter,
|
||
tx: tx,
|
||
)
|
||
let chatArchiveResult = try chatArchiver.archiveChats(
|
||
stream: stream,
|
||
context: chatArchivingContext,
|
||
)
|
||
switch chatArchiveResult {
|
||
case .success:
|
||
break
|
||
case .partialSuccess(let partialFailures):
|
||
errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false) })
|
||
case .completeFailure(let error):
|
||
errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
|
||
throw BackupError()
|
||
}
|
||
|
||
let chatItemArchiveResult = try chatItemArchiver.archiveInteractions(
|
||
stream: stream,
|
||
context: chatArchivingContext,
|
||
)
|
||
switch chatItemArchiveResult {
|
||
case .success:
|
||
break
|
||
case .partialSuccess(let partialFailures):
|
||
errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false) })
|
||
case .completeFailure(let error):
|
||
errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
|
||
throw BackupError()
|
||
}
|
||
|
||
let archivingContext = BackupArchive.ArchivingContext(
|
||
localIdentifiers: localIdentifiers,
|
||
startDate: startDate,
|
||
remoteConfig: remoteConfig,
|
||
currentUploadEra: currentUploadEra,
|
||
bencher: bencher,
|
||
attachmentByteCounter: attachmentByteCounter,
|
||
includedContentFilter: includedContentFilter,
|
||
tx: tx,
|
||
)
|
||
let stickerPackArchiveResult = try stickerPackArchiver.archiveStickerPacks(
|
||
stream: stream,
|
||
context: archivingContext,
|
||
)
|
||
switch stickerPackArchiveResult {
|
||
case .success:
|
||
break
|
||
case .partialSuccess(let partialFailures):
|
||
errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false) })
|
||
case .completeFailure(let error):
|
||
errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
|
||
throw BackupError()
|
||
}
|
||
|
||
let adHocCallArchiveResult = try adHocCallArchiver.archiveAdHocCalls(
|
||
stream: stream,
|
||
context: chatArchivingContext,
|
||
)
|
||
switch adHocCallArchiveResult {
|
||
case .success:
|
||
break
|
||
case .partialSuccess(let partialFailures):
|
||
errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false) })
|
||
case .completeFailure(let error):
|
||
errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
|
||
throw BackupError()
|
||
}
|
||
|
||
try stream.closeFileStream()
|
||
|
||
logger.info("Finished exporting backup")
|
||
bencher.logResults()
|
||
})
|
||
processErrors(errors: errors, didFail: result.isSuccess.negated)
|
||
return try result.get()
|
||
}
|
||
|
||
private func writeHeader(
|
||
stream: BackupArchiveProtoOutputStream,
|
||
backupVersion: UInt64,
|
||
startDate: Date,
|
||
currentAppVersion: String,
|
||
firstAppVersion: String,
|
||
mediaRootBackupKey: MediaRootBackupKey,
|
||
tx: DBReadTransaction,
|
||
) throws {
|
||
var backupInfo = BackupProto_BackupInfo()
|
||
backupInfo.version = backupVersion
|
||
backupInfo.backupTimeMs = startDate.ows_millisecondsSince1970
|
||
backupInfo.currentAppVersion = currentAppVersion
|
||
backupInfo.firstAppVersion = firstAppVersion
|
||
|
||
backupInfo.mediaRootBackupKey = mediaRootBackupKey.serialize()
|
||
|
||
switch stream.writeHeader(backupInfo) {
|
||
case .success:
|
||
break
|
||
case .fileIOError(let error), .protoSerializationError(let error):
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// MARK: - Import
|
||
|
||
public func backupRestoreState(tx: DBReadTransaction) -> BackupRestoreState {
|
||
let raw = kvStore.getInt(
|
||
Constants.keyValueStoreRestoreStateKey,
|
||
defaultValue: 0,
|
||
transaction: tx,
|
||
)
|
||
guard let value = BackupRestoreState(rawValue: raw) else {
|
||
owsFailDebug("Unrecognized state!")
|
||
return .none
|
||
}
|
||
return value
|
||
}
|
||
|
||
public func importEncryptedBackup(
|
||
fileUrl: URL,
|
||
localIdentifiers: LocalIdentifiers,
|
||
isPrimaryDevice: Bool,
|
||
source: BackupImportSource,
|
||
progress progressSink: OWSProgressSink?,
|
||
logger: PrefixedLogger,
|
||
) async throws {
|
||
|
||
let backupEncryptionKey = try await source.deriveBackupEncryptionKeyWithSVRBIfNeeded(
|
||
backupRequestManager: backupRequestManager,
|
||
db: db,
|
||
libsignalNet: libsignalNet,
|
||
nonceStore: backupNonceMetadataStore,
|
||
logger: logger,
|
||
)
|
||
|
||
try await _importBackup(
|
||
fileUrl: fileUrl,
|
||
localIdentifiers: localIdentifiers,
|
||
isPrimaryDevice: isPrimaryDevice,
|
||
progressSink: progressSink,
|
||
benchTitle: "Import encrypted Backup",
|
||
backupPurpose: source.libsignalPurpose,
|
||
openInputStreamBlock: { fileUrl, frameRestoreProgress, tx in
|
||
return encryptedStreamProvider.openEncryptedInputFileStream(
|
||
fileUrl: fileUrl,
|
||
source: source,
|
||
backupEncryptionKey: backupEncryptionKey,
|
||
frameRestoreProgress: frameRestoreProgress,
|
||
tx: tx,
|
||
)
|
||
},
|
||
)
|
||
}
|
||
|
||
#if TESTABLE_BUILD
|
||
public func importPlaintextBackupForTests(
|
||
fileUrl: URL,
|
||
localIdentifiers: LocalIdentifiers,
|
||
) async throws {
|
||
try await _importBackup(
|
||
fileUrl: fileUrl,
|
||
localIdentifiers: localIdentifiers,
|
||
isPrimaryDevice: true,
|
||
progressSink: nil,
|
||
benchTitle: "Import plaintext Backup",
|
||
backupPurpose: .remoteBackup,
|
||
openInputStreamBlock: { fileUrl, frameRestoreProgress, _ in
|
||
return plaintextStreamProvider.openPlaintextInputFileStream(
|
||
fileUrl: fileUrl,
|
||
frameRestoreProgress: frameRestoreProgress,
|
||
)
|
||
},
|
||
)
|
||
}
|
||
#endif
|
||
|
||
/// Everything in this method MUST be idempotent, as partial progress can be made
|
||
/// before app termination, which will result in this getting called again.
|
||
public func finalizeBackupImport(progress: OWSProgressSink?) async throws {
|
||
let oversizedTextProgress: OWSProgressSink?
|
||
if let progress {
|
||
oversizedTextProgress = await progress.addChild(
|
||
withLabel: "Import Backup: Process Oversized Text Attachments",
|
||
unitCount: 5,
|
||
)
|
||
} else {
|
||
oversizedTextProgress = nil
|
||
}
|
||
|
||
try await oversizeTextArchiver.finishRestoringOversizedTextAttachments(
|
||
progress: oversizedTextProgress,
|
||
)
|
||
|
||
await db.awaitableWrite { tx in
|
||
kvStore.setInt(
|
||
BackupRestoreState.finalized.rawValue,
|
||
key: Constants.keyValueStoreRestoreStateKey,
|
||
transaction: tx,
|
||
)
|
||
}
|
||
}
|
||
|
||
private func _importBackup(
|
||
fileUrl: URL,
|
||
localIdentifiers: LocalIdentifiers,
|
||
isPrimaryDevice: Bool,
|
||
progressSink: OWSProgressSink?,
|
||
benchTitle: String,
|
||
backupPurpose: MessageBackupPurpose,
|
||
openInputStreamBlock: (
|
||
URL,
|
||
BackupArchiveImportFramesProgress?,
|
||
DBReadTransaction,
|
||
) -> BackupArchive.ProtoStream.OpenInputStreamResult,
|
||
) async throws {
|
||
let frameRestoreProgress: BackupArchiveImportFramesProgress?
|
||
let recreateIndexesProgress: BackupArchiveImportRecreateIndexesProgress?
|
||
let finalizeProgress: OWSProgressSink?
|
||
if let progressSink {
|
||
frameRestoreProgress = try await .prepare(
|
||
sink: await progressSink.addChild(
|
||
withLabel: "Import Backup: Import Frames",
|
||
unitCount: 83,
|
||
),
|
||
fileUrl: fileUrl,
|
||
)
|
||
recreateIndexesProgress = await .prepare(
|
||
sink: await progressSink.addChild(
|
||
withLabel: "Import Backup: Recreate Indexes",
|
||
unitCount: 12,
|
||
),
|
||
)
|
||
finalizeProgress = await progressSink.addChild(
|
||
withLabel: "Import Backup: Finalize",
|
||
unitCount: 5,
|
||
)
|
||
} else {
|
||
frameRestoreProgress = nil
|
||
recreateIndexesProgress = nil
|
||
finalizeProgress = nil
|
||
}
|
||
|
||
let backupInfo = try await db.awaitableWriteWithRollbackIfThrows { tx in
|
||
return try BenchMemory(
|
||
title: benchTitle,
|
||
memorySamplerRatio: Constants.memorySamplerFrameRatio,
|
||
logInProduction: true,
|
||
) { memorySampler -> BackupProto_BackupInfo in
|
||
return try self.databaseChangeObserver.disable(tx: tx) { tx in
|
||
let inputStream: BackupArchiveProtoInputStream
|
||
switch openInputStreamBlock(fileUrl, frameRestoreProgress, tx) {
|
||
case .success(let protoStream, _):
|
||
inputStream = protoStream
|
||
case .fileNotFound:
|
||
throw OWSAssertionError("File not found!")
|
||
case .unableToOpenFileStream:
|
||
throw OWSAssertionError("Unable to open input stream!")
|
||
case .hmacValidationFailedOnEncryptedFile:
|
||
throw OWSAssertionError("HMAC validation failed!")
|
||
}
|
||
|
||
let inputFileSize = try OWSFileSystem.fileSize(of: fileUrl)
|
||
|
||
return try self._importBackup(
|
||
inputStream: inputStream,
|
||
inputFileSize: inputFileSize,
|
||
localIdentifiers: localIdentifiers,
|
||
isPrimaryDevice: isPrimaryDevice,
|
||
backupPurpose: backupPurpose,
|
||
recreateIndexesProgress: recreateIndexesProgress,
|
||
memorySampler: memorySampler,
|
||
tx: tx,
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
appVersion.didRestoreFromBackup(
|
||
backupCurrentAppVersion: backupInfo.currentAppVersion.nilIfEmpty,
|
||
backupFirstAppVersion: backupInfo.firstAppVersion.nilIfEmpty,
|
||
)
|
||
|
||
try await self.finalizeBackupImport(progress: finalizeProgress)
|
||
}
|
||
|
||
private func _importBackup(
|
||
inputStream stream: BackupArchiveProtoInputStream,
|
||
inputFileSize: UInt64,
|
||
localIdentifiers: LocalIdentifiers,
|
||
isPrimaryDevice: Bool,
|
||
backupPurpose: MessageBackupPurpose,
|
||
recreateIndexesProgress: BackupArchiveImportRecreateIndexesProgress?,
|
||
memorySampler: MemorySampler,
|
||
tx: DBWriteTransaction,
|
||
) throws -> BackupProto_BackupInfo {
|
||
let bencher = BackupArchive.RestoreBencher(
|
||
dateProviderMonotonic: dateProviderMonotonic,
|
||
memorySampler: memorySampler,
|
||
)
|
||
|
||
switch backupRestoreState(tx: tx) {
|
||
case .none:
|
||
break
|
||
case .unfinalized, .finalized:
|
||
throw OWSAssertionError("Restoring from backup twice!")
|
||
}
|
||
|
||
let startDate = dateProvider()
|
||
let remoteConfig = remoteConfigManager.currentConfig()
|
||
let attachmentByteCounter = BackupArchiveAttachmentByteCounter()
|
||
|
||
// Drops all indexes on the `TSInteraction` table before doing the
|
||
// import, which dramatically speeds up the import. We'll then recreate
|
||
// all these indexes in bulk afterwards.
|
||
let interactionIndexes = try bencher.benchPreFrameRestoreAction(.DropInteractionIndexes) {
|
||
try dropAllIndexes(
|
||
forTable: InteractionRecord.databaseTableName,
|
||
tx: tx,
|
||
)
|
||
}
|
||
|
||
var frameErrors = [LoggableErrorAndProto]()
|
||
let result = Result<BackupProto_BackupInfo, Error>(catching: {
|
||
var hasMoreFrames = false
|
||
var framesRestored: UInt64 = 0
|
||
|
||
let backupInfo: BackupProto_BackupInfo
|
||
switch stream.readHeader() {
|
||
case .success(let header, let moreBytesAvailable):
|
||
backupInfo = header
|
||
hasMoreFrames = moreBytesAvailable
|
||
framesRestored += 1
|
||
case .invalidByteLengthDelimiter:
|
||
throw OWSAssertionError("invalid byte length delimiter on header")
|
||
case .emptyFinalFrame:
|
||
throw OWSAssertionError("invalid empty header frame")
|
||
case .protoDeserializationError(let error):
|
||
// Fail if we fail to deserialize the header.
|
||
frameErrors.append(LoggableErrorAndProto(
|
||
error: BackupArchive.RestoreFrameError.restoreFrameError(
|
||
.invalidProtoData(.missingBackupInfoHeader),
|
||
BackupArchive.BackupInfoId(),
|
||
),
|
||
wasFrameDropped: true,
|
||
))
|
||
throw error
|
||
}
|
||
|
||
logger.info("Importing with version \(backupInfo.version), timestamp \(backupInfo.backupTimeMs)")
|
||
|
||
guard backupInfo.version == Constants.supportedBackupVersion else {
|
||
frameErrors.append(LoggableErrorAndProto(
|
||
error: BackupArchive.RestoreFrameError.restoreFrameError(
|
||
.invalidProtoData(.unsupportedBackupInfoVersion),
|
||
BackupArchive.BackupInfoId(),
|
||
),
|
||
wasFrameDropped: true,
|
||
protoFrame: backupInfo,
|
||
))
|
||
throw BackupImportError.unsupportedVersion
|
||
}
|
||
do {
|
||
let mrbk = try BackupKey(contents: backupInfo.mediaRootBackupKey)
|
||
localStorage.setMediaRootBackupKey(MediaRootBackupKey(backupKey: mrbk), tx: tx)
|
||
} catch {
|
||
frameErrors.append(LoggableErrorAndProto(
|
||
error: BackupArchive.RestoreFrameError.restoreFrameError(
|
||
.invalidProtoData(.invalidMediaRootBackupKey),
|
||
BackupArchive.BackupInfoId(),
|
||
),
|
||
wasFrameDropped: true,
|
||
protoFrame: backupInfo,
|
||
))
|
||
throw error
|
||
}
|
||
|
||
/// Wraps all the various "contexts" we pass to downstream archivers.
|
||
struct Contexts {
|
||
let accountData: BackupArchive.AccountDataRestoringContext
|
||
let chat: BackupArchive.ChatRestoringContext
|
||
var chatItem: BackupArchive.ChatItemRestoringContext
|
||
let customChatColor: BackupArchive.CustomChatColorRestoringContext
|
||
let recipient: BackupArchive.RecipientRestoringContext
|
||
let stickerPack: BackupArchive.RestoringContext
|
||
|
||
init(
|
||
localIdentifiers: LocalIdentifiers,
|
||
backupPurpose: MessageBackupPurpose,
|
||
startDate: Date,
|
||
remoteConfig: RemoteConfig,
|
||
attachmentByteCounter: BackupArchiveAttachmentByteCounter,
|
||
isPrimaryDevice: Bool,
|
||
tx: DBWriteTransaction,
|
||
) {
|
||
accountData = BackupArchive.AccountDataRestoringContext(
|
||
backupPurpose: backupPurpose,
|
||
localIdentifiers: localIdentifiers,
|
||
startDate: startDate,
|
||
remoteConfig: remoteConfig,
|
||
attachmentByteCounter: attachmentByteCounter,
|
||
isPrimaryDevice: isPrimaryDevice,
|
||
tx: tx,
|
||
)
|
||
customChatColor = BackupArchive.CustomChatColorRestoringContext(
|
||
accountDataContext: accountData,
|
||
localIdentifiers: localIdentifiers,
|
||
startDate: startDate,
|
||
remoteConfig: remoteConfig,
|
||
attachmentByteCounter: attachmentByteCounter,
|
||
isPrimaryDevice: isPrimaryDevice,
|
||
tx: tx,
|
||
)
|
||
recipient = BackupArchive.RecipientRestoringContext(
|
||
localIdentifiers: localIdentifiers,
|
||
startDate: startDate,
|
||
remoteConfig: remoteConfig,
|
||
attachmentByteCounter: attachmentByteCounter,
|
||
isPrimaryDevice: isPrimaryDevice,
|
||
tx: tx,
|
||
)
|
||
chat = BackupArchive.ChatRestoringContext(
|
||
customChatColorContext: customChatColor,
|
||
recipientContext: recipient,
|
||
localIdentifiers: localIdentifiers,
|
||
startDate: startDate,
|
||
remoteConfig: remoteConfig,
|
||
attachmentByteCounter: attachmentByteCounter,
|
||
isPrimaryDevice: isPrimaryDevice,
|
||
tx: tx,
|
||
)
|
||
chatItem = BackupArchive.ChatItemRestoringContext(
|
||
accountDataContext: accountData,
|
||
chatContext: chat,
|
||
recipientContext: recipient,
|
||
localIdentifiers: localIdentifiers,
|
||
startDate: startDate,
|
||
remoteConfig: remoteConfig,
|
||
attachmentByteCounter: attachmentByteCounter,
|
||
isPrimaryDevice: isPrimaryDevice,
|
||
tx: tx,
|
||
)
|
||
stickerPack = BackupArchive.RestoringContext(
|
||
localIdentifiers: localIdentifiers,
|
||
startDate: startDate,
|
||
remoteConfig: remoteConfig,
|
||
attachmentByteCounter: attachmentByteCounter,
|
||
isPrimaryDevice: isPrimaryDevice,
|
||
tx: tx,
|
||
)
|
||
}
|
||
}
|
||
let contexts = Contexts(
|
||
localIdentifiers: localIdentifiers,
|
||
backupPurpose: backupPurpose,
|
||
startDate: startDate,
|
||
remoteConfig: remoteConfig,
|
||
attachmentByteCounter: attachmentByteCounter,
|
||
isPrimaryDevice: isPrimaryDevice,
|
||
tx: tx,
|
||
)
|
||
|
||
while hasMoreFrames {
|
||
try Task.checkCancellation()
|
||
try autoreleasepool {
|
||
let frame: BackupProto_Frame?
|
||
switch stream.readFrame() {
|
||
case let .success(_frame, moreBytesAvailable):
|
||
frame = _frame
|
||
hasMoreFrames = moreBytesAvailable
|
||
framesRestored += 1
|
||
case .invalidByteLengthDelimiter:
|
||
throw OWSAssertionError("invalid byte length delimiter on header")
|
||
case .emptyFinalFrame:
|
||
frame = nil
|
||
hasMoreFrames = false
|
||
case .protoDeserializationError(let error):
|
||
// fail the whole thing if we fail to deserialize one frame
|
||
owsFailDebug("Failed to deserialize proto frame!")
|
||
if BuildFlags.Backups.restoreFailOnAnyError {
|
||
throw error
|
||
} else {
|
||
return
|
||
}
|
||
}
|
||
|
||
guard
|
||
let frame,
|
||
let frameItem = frame.item
|
||
else {
|
||
if hasMoreFrames {
|
||
frameErrors.append(LoggableErrorAndProto(
|
||
error: BackupArchive.UnrecognizedEnumError(
|
||
enumType: BackupProto_Frame.OneOf_Item.self,
|
||
),
|
||
wasFrameDropped: true,
|
||
))
|
||
}
|
||
return
|
||
}
|
||
|
||
try bencher.processFrame { frameBencher in
|
||
defer {
|
||
frameBencher.didProcessFrame(frame)
|
||
}
|
||
|
||
switch frameItem {
|
||
case .recipient(let recipient):
|
||
let recipientResult: BackupArchive.RestoreFrameResult<BackupArchive.RecipientId>
|
||
switch recipient.destination {
|
||
case nil:
|
||
recipientResult = .unrecognizedEnum(BackupArchive.UnrecognizedEnumError(
|
||
enumType: BackupProto_Recipient.OneOf_Destination.self,
|
||
))
|
||
case .self_p(let selfRecipientProto):
|
||
recipientResult = localRecipientArchiver.restoreSelfRecipient(
|
||
selfRecipientProto,
|
||
recipient: recipient,
|
||
context: contexts.recipient,
|
||
)
|
||
case .contact(let contactRecipientProto):
|
||
recipientResult = contactRecipientArchiver.restoreContactRecipientProto(
|
||
contactRecipientProto,
|
||
recipient: recipient,
|
||
context: contexts.recipient,
|
||
)
|
||
case .group(let groupRecipientProto):
|
||
recipientResult = groupRecipientArchiver.restoreGroupRecipientProto(
|
||
groupRecipientProto,
|
||
recipient: recipient,
|
||
context: contexts.recipient,
|
||
)
|
||
case .distributionList(let distributionListRecipientProto):
|
||
recipientResult = distributionListRecipientArchiver.restoreDistributionListRecipientProto(
|
||
distributionListRecipientProto,
|
||
recipient: recipient,
|
||
context: contexts.recipient,
|
||
)
|
||
case .releaseNotes(let releaseNotesRecipientProto):
|
||
recipientResult = releaseNotesRecipientArchiver.restoreReleaseNotesRecipientProto(
|
||
releaseNotesRecipientProto,
|
||
recipient: recipient,
|
||
context: contexts.recipient,
|
||
)
|
||
case .callLink(let callLinkRecipientProto):
|
||
recipientResult = callLinkRecipientArchiver.restoreCallLinkRecipientProto(
|
||
callLinkRecipientProto,
|
||
recipient: recipient,
|
||
context: contexts.recipient,
|
||
)
|
||
}
|
||
|
||
switch recipientResult {
|
||
case .success:
|
||
return
|
||
case .unrecognizedEnum(let error):
|
||
frameErrors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true, protoFrame: recipient))
|
||
return
|
||
case .partialRestore(let errors):
|
||
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false, protoFrame: recipient) })
|
||
case .failure(let errors):
|
||
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: true, protoFrame: recipient) })
|
||
if BuildFlags.Backups.restoreFailOnAnyError {
|
||
throw BackupError()
|
||
}
|
||
}
|
||
case .chat(let chat):
|
||
let chatResult = chatArchiver.restore(
|
||
chat,
|
||
context: contexts.chat,
|
||
)
|
||
switch chatResult {
|
||
case .success:
|
||
return
|
||
case .unrecognizedEnum(let error):
|
||
frameErrors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true, protoFrame: chat))
|
||
return
|
||
case .partialRestore(let errors):
|
||
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false, protoFrame: chat) })
|
||
case .failure(let errors):
|
||
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: true, protoFrame: chat) })
|
||
if BuildFlags.Backups.restoreFailOnAnyError {
|
||
throw BackupError()
|
||
}
|
||
}
|
||
case .chatItem(let chatItem):
|
||
let chatItemResult = chatItemArchiver.restore(
|
||
chatItem,
|
||
context: contexts.chatItem,
|
||
)
|
||
switch chatItemResult {
|
||
case .success:
|
||
return
|
||
case .unrecognizedEnum(let error):
|
||
frameErrors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true, protoFrame: chatItem))
|
||
return
|
||
case .partialRestore(let errors):
|
||
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false, protoFrame: chatItem) })
|
||
case .failure(let errors):
|
||
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: true, protoFrame: chatItem) })
|
||
if BuildFlags.Backups.restoreFailOnAnyError {
|
||
throw BackupError()
|
||
}
|
||
}
|
||
case .account(let backupProtoAccountData):
|
||
let accountDataResult = accountDataArchiver.restore(
|
||
backupProtoAccountData,
|
||
context: contexts.accountData,
|
||
chatColorsContext: contexts.customChatColor,
|
||
chatItemContext: contexts.chatItem,
|
||
)
|
||
switch accountDataResult {
|
||
case .success:
|
||
return
|
||
case .unrecognizedEnum(let error):
|
||
frameErrors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true, protoFrame: backupProtoAccountData))
|
||
return
|
||
case .partialRestore(let errors):
|
||
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false, protoFrame: backupProtoAccountData) })
|
||
case .failure(let errors):
|
||
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: true, protoFrame: backupProtoAccountData) })
|
||
// We always fail if we fail to import account data, even in prod.
|
||
throw BackupError()
|
||
}
|
||
case .stickerPack(let backupProtoStickerPack):
|
||
let stickerPackResult = stickerPackArchiver.restore(
|
||
backupProtoStickerPack,
|
||
context: contexts.stickerPack,
|
||
)
|
||
switch stickerPackResult {
|
||
case .success:
|
||
return
|
||
case .unrecognizedEnum(let error):
|
||
frameErrors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true, protoFrame: backupProtoStickerPack))
|
||
return
|
||
case .partialRestore(let errors):
|
||
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false, protoFrame: backupProtoStickerPack) })
|
||
case .failure(let errors):
|
||
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: true, protoFrame: backupProtoStickerPack) })
|
||
if BuildFlags.Backups.restoreFailOnAnyError {
|
||
throw BackupError()
|
||
}
|
||
}
|
||
case .adHocCall(let backupProtoAdHocCall):
|
||
let adHocCallResult = adHocCallArchiver.restore(
|
||
backupProtoAdHocCall,
|
||
context: contexts.chatItem,
|
||
)
|
||
switch adHocCallResult {
|
||
case .success:
|
||
return
|
||
case .unrecognizedEnum(let error):
|
||
frameErrors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true, protoFrame: backupProtoAdHocCall))
|
||
return
|
||
case .partialRestore(let errors):
|
||
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false, protoFrame: backupProtoAdHocCall) })
|
||
case .failure(let errors):
|
||
frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: true, protoFrame: backupProtoAdHocCall) })
|
||
if BuildFlags.Backups.restoreFailOnAnyError {
|
||
throw BackupError()
|
||
}
|
||
}
|
||
case .notificationProfile:
|
||
// Notification profiles are unsupported on iOS and
|
||
// we do not even round trip them per spec.
|
||
break
|
||
case .chatFolder:
|
||
// Chat folders are unsupported on iOS and
|
||
// we do not even round trip them per spec.
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
stream.closeFileStream()
|
||
|
||
// Now that we've imported successfully, we want to recreate the
|
||
// the indexes we temporarily dropped.
|
||
recreateIndexesProgress?.willStartIndexRecreation(totalFramesRestored: framesRestored)
|
||
try bencher.benchPostFrameRestoreAction(.RecreateInteractionIndexes) {
|
||
try createIndexes(
|
||
interactionIndexes,
|
||
onTable: InteractionRecord.databaseTableName,
|
||
tx: tx,
|
||
)
|
||
}
|
||
recreateIndexesProgress?.didFinishIndexRecreation()
|
||
|
||
// Take any necessary post-frame-restore actions.
|
||
try postFrameRestoreActionManager.performPostFrameRestoreActions(
|
||
recipientActions: contexts.recipient.postFrameRestoreActions,
|
||
chatActions: contexts.chat.postFrameRestoreActions,
|
||
bencher: bencher,
|
||
chatItemContext: contexts.chatItem,
|
||
)
|
||
|
||
// Index threads synchronously, since that should be fast.
|
||
bencher.benchPostFrameRestoreAction(.IndexThreads) {
|
||
fullTextSearchIndexer.indexThreads(tx: tx)
|
||
}
|
||
|
||
// Schedule background message indexing, since that'll be slow.
|
||
try fullTextSearchIndexer.scheduleMessagesJob(tx: tx)
|
||
|
||
// Record that we've restored a Backup!
|
||
kvStore.setInt(
|
||
BackupRestoreState.unfinalized.rawValue,
|
||
key: Constants.keyValueStoreRestoreStateKey,
|
||
transaction: tx,
|
||
)
|
||
|
||
// Populate "last Backup" details, since otherwise they'll be blank
|
||
// and imply the user has no Backup.
|
||
backupSettingsStore.setLastBackupDetails(
|
||
date: Date(millisecondsSince1970: backupInfo.backupTimeMs),
|
||
backupFileSizeBytes: inputFileSize,
|
||
backupMediaSizeBytes: attachmentByteCounter.attachmentByteSize(),
|
||
tx: tx,
|
||
)
|
||
|
||
tx.addSyncCompletion { [self] in
|
||
Task {
|
||
// Kick off avatar fetches enqueued during restore.
|
||
try await avatarFetcher.runIfNeeded()
|
||
}
|
||
|
||
Task {
|
||
// Kick off attachment downloads enqueued during restore.
|
||
try await backupAttachmentCoordinator.restoreAttachmentsIfNeeded()
|
||
}
|
||
|
||
// We may have inserted disappearing messages, so we need to let
|
||
// the expiration job know.
|
||
disappearingMessagesExpirationJob.restart()
|
||
}
|
||
|
||
logger.info("Imported with version \(backupInfo.version), timestamp \(backupInfo.backupTimeMs)")
|
||
logger.info("Backup app version: \(backupInfo.currentAppVersion.nilIfEmpty ?? "Missing!")")
|
||
logger.info("Backup first app version: \(backupInfo.firstAppVersion.nilIfEmpty ?? "Missing!")")
|
||
bencher.logResults()
|
||
|
||
return backupInfo
|
||
})
|
||
|
||
processErrors(errors: frameErrors, didFail: result.isSuccess.negated)
|
||
return try result.get()
|
||
}
|
||
|
||
// MARK: -
|
||
|
||
private struct SQLiteIndexInfo {
|
||
let tableName: String
|
||
let sqlThatCreatedIndex: String
|
||
}
|
||
|
||
private func dropAllIndexes(
|
||
forTable tableName: String,
|
||
tx: DBWriteTransaction,
|
||
) throws -> [SQLiteIndexInfo] {
|
||
let allIndexesOnTable: [GRDB.IndexInfo] = try tx.database.indexes(on: tableName)
|
||
|
||
var sqliteIndexInfos = [SQLiteIndexInfo]()
|
||
|
||
for index in allIndexesOnTable {
|
||
if index.name.contains("autoindex") {
|
||
// Skip indexes automatically created by SQLite, such as on
|
||
// primary keys.
|
||
continue
|
||
}
|
||
|
||
guard
|
||
let sqlThatCreatedIndex = try String.fetchOne(
|
||
tx.database,
|
||
sql: """
|
||
SELECT sql FROM sqlite_master
|
||
WHERE type = 'index'
|
||
AND name = '\(index.name)'
|
||
""",
|
||
)
|
||
else {
|
||
throw OWSAssertionError("Failed to get SQL for creating index \(index.name)!")
|
||
}
|
||
|
||
sqliteIndexInfos.append(SQLiteIndexInfo(
|
||
tableName: tableName,
|
||
sqlThatCreatedIndex: sqlThatCreatedIndex,
|
||
))
|
||
|
||
try tx.database.drop(index: index.name)
|
||
}
|
||
|
||
return sqliteIndexInfos
|
||
}
|
||
|
||
private func createIndexes(
|
||
_ indexInfos: [SQLiteIndexInfo],
|
||
onTable tableName: String,
|
||
tx: DBWriteTransaction,
|
||
) throws {
|
||
owsPrecondition(indexInfos.allSatisfy { $0.tableName == tableName })
|
||
|
||
for indexInfo in indexInfos {
|
||
try tx.database.execute(sql: indexInfo.sqlThatCreatedIndex)
|
||
}
|
||
}
|
||
|
||
// MARK: -
|
||
|
||
private func processErrors(
|
||
errors: [LoggableErrorAndProto],
|
||
didFail: Bool,
|
||
) {
|
||
let collapsedErrors = BackupArchive.collapse(errors)
|
||
var maxLogLevel = -1
|
||
var wasFrameDropped = false
|
||
collapsedErrors.forEach { collapsedError in
|
||
collapsedError.log()
|
||
maxLogLevel = max(maxLogLevel, collapsedError.logLevel.rawValue)
|
||
if collapsedError.wasFrameDropped {
|
||
wasFrameDropped = true
|
||
}
|
||
}
|
||
if wasFrameDropped {
|
||
// Log this specifically so we can do a naive exact text search in debug logs.
|
||
logger.error("Dropped frame(s) on backup export or import!!!")
|
||
}
|
||
// Only present errors if some error rises above warning.
|
||
// (But if one does, present _all_ errors).
|
||
if maxLogLevel > BackupArchive.LogLevel.warning.rawValue {
|
||
Task {
|
||
await db.awaitableWrite { tx in
|
||
backupArchiveErrorPresenter.persistErrors(collapsedErrors, didFail: didFail, tx: tx)
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
private func validateEncryptedBackup(
|
||
fileUrl: URL,
|
||
backupEncryptionKey: MessageBackupKey,
|
||
backupPurpose: MessageBackupPurpose,
|
||
) async throws {
|
||
let fileSize = (try? OWSFileSystem.fileSize(ofPath: fileUrl.path)) ?? 0
|
||
|
||
do {
|
||
let result = try validateMessageBackup(key: backupEncryptionKey, purpose: backupPurpose, length: fileSize) {
|
||
return try FileHandle(forReadingFrom: fileUrl)
|
||
}
|
||
if result.fields.count > 0 {
|
||
throw BackupValidationError.unknownFields(result.fields)
|
||
}
|
||
} catch {
|
||
switch error {
|
||
case let validationError as MessageBackupValidationError:
|
||
await backupArchiveErrorPresenter.persistValidationError(validationError)
|
||
logger.error("Backup validation failed \(validationError.errorMessage)")
|
||
throw BackupValidationError.validationFailed(
|
||
message: validationError.errorMessage,
|
||
unknownFields: validationError.unknownFields.fields,
|
||
)
|
||
case SignalError.ioError(let description):
|
||
logger.error("Backup validation i/o error: \(description)")
|
||
throw BackupValidationError.ioError(description)
|
||
default:
|
||
logger.error("Backup validation unknown error: \(error)")
|
||
throw BackupValidationError.unknownError
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: -
|
||
|
||
public func scheduleRestoreFromSVRBBeforeNextExport(tx: DBWriteTransaction) {
|
||
kvStore.setBool(
|
||
true,
|
||
key: Constants.keyValueStoreNeedForwardSecrecyTokenFetchKey,
|
||
transaction: tx,
|
||
)
|
||
}
|
||
|
||
private func needsRestoreFromSVRBBeforeRemoteExport(tx: DBReadTransaction) -> Bool {
|
||
kvStore.getBool(
|
||
Constants.keyValueStoreNeedForwardSecrecyTokenFetchKey,
|
||
defaultValue: false,
|
||
transaction: tx,
|
||
)
|
||
}
|
||
|
||
private func fetchRemoteSVRBForwardSecrecyToken(
|
||
key: MessageRootBackupKey,
|
||
auth: ChatServiceAuth,
|
||
logger: PrefixedLogger,
|
||
) async throws {
|
||
let backupServiceAuth = try await backupRequestManager.fetchBackupServiceAuthForRegistration(
|
||
key: key,
|
||
localAci: key.aci,
|
||
chatServiceAuth: auth,
|
||
logger: logger,
|
||
)
|
||
|
||
let metadataHeader: BackupNonce.MetadataHeader
|
||
do {
|
||
metadataHeader = try await backupCdnInfo(
|
||
backupKey: key,
|
||
backupAuth: backupServiceAuth,
|
||
logger: logger,
|
||
).metadataHeader
|
||
} catch let error as OWSHTTPError where error.responseStatusCode == 404 {
|
||
// If no backup is found, treat this as unrecoverable
|
||
throw SVRBError.unrecoverable
|
||
}
|
||
|
||
let nonceSource = BackupImportSource.NonceMetadataSource.svrB(header: metadataHeader, auth: auth)
|
||
let source = BackupImportSource.remote(key: key, nonceSource: nonceSource)
|
||
_ = try await source.deriveBackupEncryptionKeyWithSVRBIfNeeded(
|
||
backupRequestManager: backupRequestManager,
|
||
db: db,
|
||
libsignalNet: libsignalNet,
|
||
nonceStore: backupNonceMetadataStore,
|
||
logger: logger,
|
||
)
|
||
}
|
||
}
|