Signal-iOS/SignalServiceKit/Backups/Archiving/BackupArchiveManagerImpl.swift
2026-03-26 17:10:38 -05:00

1564 lines
69 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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,
)
}
}