Signal-iOS/SignalServiceKit/MessageBackup/MessageBackupManagerImpl.swift
2024-09-25 11:38:51 -05:00

760 lines
32 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import LibSignalClient
public enum BackupValidationError: Error {
case unknownFields([String])
case validationFailed(message: String, unknownFields: [String])
case ioError(String)
case unknownError
}
public class MessageBackupManagerImpl: MessageBackupManager {
private enum Constants {
static let keyValueStoreCollectionName = "MessageBackupManager"
static let keyValueStoreHasReservedBackupKey = "HasReservedBackupKey"
static let supportedBackupVersion: UInt64 = 1
}
private class NotImplementedError: Error {}
private class BackupError: Error {}
private class BackupVersionNotSupportedError: Error {}
private let accountDataArchiver: MessageBackupAccountDataArchiver
private let attachmentDownloadManager: AttachmentDownloadManager
private let attachmentUploadManager: AttachmentUploadManager
private let backupAttachmentDownloadStore: BackupAttachmentDownloadStore
private let backupAttachmentUploadManager: BackupAttachmentUploadManager
private let backupRequestManager: MessageBackupRequestManager
private let backupStickerPackDownloadStore: BackupStickerPackDownloadStore
private let chatArchiver: MessageBackupChatArchiver
private let chatItemArchiver: MessageBackupChatItemArchiver
private let contactRecipientArchiver: MessageBackupContactRecipientArchiver
private let dateProvider: DateProvider
private let db: DB
private let distributionListRecipientArchiver: MessageBackupDistributionListRecipientArchiver
private let encryptedStreamProvider: MessageBackupEncryptedProtoStreamProvider
private let groupRecipientArchiver: MessageBackupGroupRecipientArchiver
private let incrementalTSAttachmentMigrator: IncrementalMessageTSAttachmentMigrator
private let kvStore: KeyValueStore
private let localRecipientArchiver: MessageBackupLocalRecipientArchiver
private let messageBackupKeyMaterial: MessageBackupKeyMaterial
private let plaintextStreamProvider: MessageBackupPlaintextProtoStreamProvider
private let postFrameRestoreActionManager: MessageBackupPostFrameRestoreActionManager
private let releaseNotesRecipientArchiver: MessageBackupReleaseNotesRecipientArchiver
private let stickerPackArchiver: MessageBackupStickerPackArchiver
public init(
accountDataArchiver: MessageBackupAccountDataArchiver,
attachmentDownloadManager: AttachmentDownloadManager,
attachmentUploadManager: AttachmentUploadManager,
backupAttachmentDownloadStore: BackupAttachmentDownloadStore,
backupAttachmentUploadManager: BackupAttachmentUploadManager,
backupRequestManager: MessageBackupRequestManager,
backupStickerPackDownloadStore: BackupStickerPackDownloadStore,
chatArchiver: MessageBackupChatArchiver,
chatItemArchiver: MessageBackupChatItemArchiver,
contactRecipientArchiver: MessageBackupContactRecipientArchiver,
dateProvider: @escaping DateProvider,
db: DB,
distributionListRecipientArchiver: MessageBackupDistributionListRecipientArchiver,
encryptedStreamProvider: MessageBackupEncryptedProtoStreamProvider,
groupRecipientArchiver: MessageBackupGroupRecipientArchiver,
incrementalTSAttachmentMigrator: IncrementalMessageTSAttachmentMigrator,
kvStoreFactory: KeyValueStoreFactory,
localRecipientArchiver: MessageBackupLocalRecipientArchiver,
messageBackupKeyMaterial: MessageBackupKeyMaterial,
plaintextStreamProvider: MessageBackupPlaintextProtoStreamProvider,
postFrameRestoreActionManager: MessageBackupPostFrameRestoreActionManager,
releaseNotesRecipientArchiver: MessageBackupReleaseNotesRecipientArchiver,
stickerPackArchiver: MessageBackupStickerPackArchiver
) {
self.accountDataArchiver = accountDataArchiver
self.attachmentDownloadManager = attachmentDownloadManager
self.attachmentUploadManager = attachmentUploadManager
self.backupAttachmentDownloadStore = backupAttachmentDownloadStore
self.backupAttachmentUploadManager = backupAttachmentUploadManager
self.backupRequestManager = backupRequestManager
self.backupStickerPackDownloadStore = backupStickerPackDownloadStore
self.chatArchiver = chatArchiver
self.chatItemArchiver = chatItemArchiver
self.contactRecipientArchiver = contactRecipientArchiver
self.dateProvider = dateProvider
self.db = db
self.distributionListRecipientArchiver = distributionListRecipientArchiver
self.encryptedStreamProvider = encryptedStreamProvider
self.groupRecipientArchiver = groupRecipientArchiver
self.incrementalTSAttachmentMigrator = incrementalTSAttachmentMigrator
self.kvStore = kvStoreFactory.keyValueStore(collection: Constants.keyValueStoreCollectionName)
self.localRecipientArchiver = localRecipientArchiver
self.messageBackupKeyMaterial = messageBackupKeyMaterial
self.plaintextStreamProvider = plaintextStreamProvider
self.postFrameRestoreActionManager = postFrameRestoreActionManager
self.releaseNotesRecipientArchiver = releaseNotesRecipientArchiver
self.stickerPackArchiver = stickerPackArchiver
}
// MARK: - Remote backups
/// Initialize Message Backups by reserving a backup ID and registering a public key used to sign backup auth credentials.
/// These registration calls are safe to call multiple times, but to avoid unecessary network calls, the app will remember if
/// backups have been successfully registered on this device and will no-op in this case.
private func reserveAndRegister(localIdentifiers: LocalIdentifiers, auth: ChatServiceAuth) async throws {
guard db.read(block: { tx in
return kvStore.getBool(Constants.keyValueStoreHasReservedBackupKey, transaction: tx) ?? false
}).negated else {
return
}
// Both reserveBackupId and registerBackupKeys can be called multiple times, so if
// we think the backupId needs to be registered, register the public key at the same time.
try await backupRequestManager.reserveBackupId(localAci: localIdentifiers.aci, auth: auth)
let backupAuth = try await backupRequestManager.fetchBackupServiceAuth(localAci: localIdentifiers.aci, auth: auth)
try await backupRequestManager.registerBackupKeys(auth: backupAuth)
// Remember this device has registered for backups
await db.awaitableWrite { [weak self] tx in
self?.kvStore.setBool(true, key: Constants.keyValueStoreHasReservedBackupKey, transaction: tx)
}
}
public func downloadEncryptedBackup(localIdentifiers: LocalIdentifiers, auth: ChatServiceAuth) async throws -> URL {
let backupAuth = try await backupRequestManager.fetchBackupServiceAuth(localAci: localIdentifiers.aci, auth: auth)
let metadata = try await backupRequestManager.fetchBackupRequestMetadata(auth: backupAuth)
let tmpFileUrl = try await attachmentDownloadManager.downloadBackup(metadata: metadata).awaitable()
// 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 uploadEncryptedBackup(
metadata: Upload.EncryptedBackupUploadMetadata,
localIdentifiers: LocalIdentifiers,
auth: ChatServiceAuth
) async throws -> Upload.Result<Upload.EncryptedBackupUploadMetadata> {
// This will return early if this device has already registered the backup ID.
try await reserveAndRegister(localIdentifiers: localIdentifiers, auth: auth)
let backupAuth = try await backupRequestManager.fetchBackupServiceAuth(localAci: localIdentifiers.aci, auth: auth)
let form = try await backupRequestManager.fetchBackupUploadForm(auth: backupAuth)
return try await attachmentUploadManager.uploadBackup(localUploadMetadata: metadata, form: form)
}
// MARK: - Export
public func exportEncryptedBackup(
localIdentifiers: LocalIdentifiers
) async throws -> Upload.EncryptedBackupUploadMetadata {
guard FeatureFlags.messageBackupFileAlpha else {
owsFailDebug("Should not be able to use backups!")
throw NotImplementedError()
}
await migrateAttachmentsBeforeBackup()
return try await db.awaitableWrite { tx in
let outputStream: MessageBackupProtoOutputStream
let metadataProvider: MessageBackup.ProtoStream.EncryptionMetadataProvider
switch self.encryptedStreamProvider.openEncryptedOutputFileStream(
localAci: localIdentifiers.aci,
tx: tx
) {
case let .success(_outputStream, _metadataProvider):
outputStream = _outputStream
metadataProvider = _metadataProvider
case .unableToOpenFileStream:
throw OWSAssertionError("Unable to open output stream")
}
try self._exportBackup(
outputStream: outputStream,
localIdentifiers: localIdentifiers,
tx: tx
)
return try metadataProvider()
}
}
public func exportPlaintextBackup(
localIdentifiers: LocalIdentifiers
) async throws -> URL {
guard FeatureFlags.messageBackupFileAlpha else {
owsFailDebug("Should not be able to use backups!")
throw NotImplementedError()
}
await migrateAttachmentsBeforeBackup()
return try await db.awaitableWrite { tx in
let outputStream: MessageBackupProtoOutputStream
let fileUrl: URL
switch self.plaintextStreamProvider.openPlaintextOutputFileStream() {
case .success(let _outputStream, let _fileUrl):
outputStream = _outputStream
fileUrl = _fileUrl
case .unableToOpenFileStream:
throw OWSAssertionError("Unable to open output file stream!")
}
try self._exportBackup(
outputStream: outputStream,
localIdentifiers: localIdentifiers,
tx: tx
)
return fileUrl
}
}
private func _exportBackup(
outputStream stream: MessageBackupProtoOutputStream,
localIdentifiers: LocalIdentifiers,
tx: DBWriteTransaction
) throws {
try writeHeader(stream: stream, tx: tx)
let currentBackupAttachmentUploadEra: String?
if MessageBackupMessageAttachmentArchiver.isFreeTierBackup() {
currentBackupAttachmentUploadEra = nil
} else {
currentBackupAttachmentUploadEra = try MessageBackupMessageAttachmentArchiver.uploadEra()
}
let customChatColorContext = MessageBackup.CustomChatColorArchivingContext(
currentBackupAttachmentUploadEra: currentBackupAttachmentUploadEra,
backupAttachmentUploadManager: backupAttachmentUploadManager,
tx: tx
)
let accountDataResult = accountDataArchiver.archiveAccountData(
stream: stream,
context: customChatColorContext
)
switch accountDataResult {
case .success:
break
case .failure(let error):
MessageBackup.log([error])
throw OWSAssertionError("Failed to archive account data")
}
let localRecipientResult = localRecipientArchiver.archiveLocalRecipient(
stream: stream
)
let localRecipientId: MessageBackup.RecipientId
switch localRecipientResult {
case .success(let success):
localRecipientId = success
case .failure(let error):
MessageBackup.log([error])
throw OWSAssertionError("Failed to archive local recipient!")
}
let recipientArchivingContext = MessageBackup.RecipientArchivingContext(
currentBackupAttachmentUploadEra: currentBackupAttachmentUploadEra,
backupAttachmentUploadManager: backupAttachmentUploadManager,
localIdentifiers: localIdentifiers,
localRecipientId: localRecipientId,
tx: tx
)
switch releaseNotesRecipientArchiver.archiveReleaseNotesRecipient(
stream: stream,
context: recipientArchivingContext
) {
case .success:
break
case .failure(let error):
MessageBackup.log([error])
throw OWSAssertionError("Failed to archive release notes channel!")
}
switch contactRecipientArchiver.archiveAllContactRecipients(
stream: stream,
context: recipientArchivingContext
) {
case .success:
break
case .partialSuccess(let partialFailures):
try processArchiveFrameErrors(errors: partialFailures)
case .completeFailure(let error):
try processFatalArchivingError(error: error)
}
switch groupRecipientArchiver.archiveAllGroupRecipients(
stream: stream,
context: recipientArchivingContext
) {
case .success:
break
case .partialSuccess(let partialFailures):
try processArchiveFrameErrors(errors: partialFailures)
case .completeFailure(let error):
try processFatalArchivingError(error: error)
}
switch distributionListRecipientArchiver.archiveAllDistributionListRecipients(
stream: stream,
context: recipientArchivingContext
) {
case .success:
break
case .partialSuccess(let partialFailures):
try processArchiveFrameErrors(errors: partialFailures)
case .completeFailure(let error):
try processFatalArchivingError(error: error)
}
// TODO: [Backups] Archive call link recipients.
let chatArchivingContext = MessageBackup.ChatArchivingContext(
currentBackupAttachmentUploadEra: currentBackupAttachmentUploadEra,
backupAttachmentUploadManager: backupAttachmentUploadManager,
customChatColorContext: customChatColorContext,
recipientContext: recipientArchivingContext,
tx: tx
)
let chatArchiveResult = chatArchiver.archiveChats(
stream: stream,
context: chatArchivingContext
)
switch chatArchiveResult {
case .success:
break
case .partialSuccess(let partialFailures):
try processArchiveFrameErrors(errors: partialFailures)
case .completeFailure(let error):
try processFatalArchivingError(error: error)
}
let chatItemArchiveResult = chatItemArchiver.archiveInteractions(
stream: stream,
context: chatArchivingContext
)
switch chatItemArchiveResult {
case .success:
break
case .partialSuccess(let partialFailures):
try processArchiveFrameErrors(errors: partialFailures)
case .completeFailure(let error):
try processFatalArchivingError(error: error)
}
let archivingContext = MessageBackup.ArchivingContext(
currentBackupAttachmentUploadEra: currentBackupAttachmentUploadEra,
backupAttachmentUploadManager: backupAttachmentUploadManager,
tx: tx
)
let stickerPackArchiveResult = stickerPackArchiver.archiveStickerPacks(
stream: stream,
context: archivingContext
)
switch stickerPackArchiveResult {
case .success:
break
case .partialSuccess(let partialFailures):
try processArchiveFrameErrors(errors: partialFailures)
case .completeFailure(let error):
try processFatalArchivingError(error: error)
}
try stream.closeFileStream()
tx.addAsyncCompletion(on: DispatchQueue.global()) { [backupAttachmentUploadManager] in
Task {
// TODO: [Backups] this needs to talk to the banner at the top of the chat
// list to show progress.
try await backupAttachmentUploadManager.backUpAllAttachments()
}
}
}
private func writeHeader(stream: MessageBackupProtoOutputStream, tx: DBWriteTransaction) throws {
var backupInfo = BackupProto_BackupInfo()
backupInfo.version = Constants.supportedBackupVersion
backupInfo.backupTimeMs = dateProvider().ows_millisecondsSince1970
switch stream.writeHeader(backupInfo) {
case .success:
break
case .fileIOError(let error), .protoSerializationError(let error):
throw error
}
}
private func processArchiveFrameErrors<IdType>(
errors: [MessageBackup.ArchiveFrameError<IdType>]
) throws {
MessageBackup.log(errors)
// At time of writing, we want to fail for every single error.
if errors.isEmpty.negated {
throw BackupError()
}
}
private func processFatalArchivingError(
error: MessageBackup.FatalArchivingError
) throws {
MessageBackup.log([error])
throw BackupError()
}
// MARK: - Import
public func importEncryptedBackup(fileUrl: URL, localIdentifiers: LocalIdentifiers) async throws {
guard FeatureFlags.messageBackupFileAlpha else {
owsFailDebug("Should not be able to use backups!")
throw NotImplementedError()
}
await migrateAttachmentsBeforeBackup()
try await db.awaitableWrite { tx in
let inputStream: MessageBackupProtoInputStream
switch self.encryptedStreamProvider.openEncryptedInputFileStream(
fileUrl: fileUrl,
localAci: localIdentifiers.aci,
tx: 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 on encrypted file!")
}
try self._importBackup(
inputStream: inputStream,
localIdentifiers: localIdentifiers,
tx: tx
)
}
}
public func importPlaintextBackup(
fileUrl: URL,
localIdentifiers: LocalIdentifiers
) async throws {
guard FeatureFlags.messageBackupFileAlpha else {
owsFailDebug("Should not be able to use backups!")
throw NotImplementedError()
}
await migrateAttachmentsBeforeBackup()
try await db.awaitableWrite { tx in
let inputStream: MessageBackupProtoInputStream
switch self.plaintextStreamProvider.openPlaintextInputFileStream(
fileUrl: fileUrl
) {
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: how did this happen for a plaintext backup?")
}
try self._importBackup(
inputStream: inputStream,
localIdentifiers: localIdentifiers,
tx: tx
)
}
}
private func _importBackup(
inputStream stream: MessageBackupProtoInputStream,
localIdentifiers: LocalIdentifiers,
tx: DBWriteTransaction
) throws {
let backupInfo: BackupProto_BackupInfo
var hasMoreFrames = false
switch stream.readHeader() {
case .success(let header, let moreBytesAvailable):
backupInfo = header
hasMoreFrames = moreBytesAvailable
case .invalidByteLengthDelimiter:
throw OWSAssertionError("invalid byte length delimiter on header")
case .protoDeserializationError(let error):
// Fail if we fail to deserialize the header.
throw error
}
Logger.info("Reading backup with version: \(backupInfo.version) backed up at \(backupInfo.backupTimeMs)")
guard backupInfo.version == Constants.supportedBackupVersion else {
throw BackupVersionNotSupportedError()
}
/// Wraps all the various "contexts" we pass to downstream archivers.
struct Contexts {
let chat: MessageBackup.ChatRestoringContext
let customChatColor: MessageBackup.CustomChatColorRestoringContext
let recipient: MessageBackup.RecipientRestoringContext
var all: [MessageBackup.RestoringContext] {
[chat, customChatColor, recipient]
}
init(localIdentifiers: LocalIdentifiers, tx: DBWriteTransaction) {
customChatColor = MessageBackup.CustomChatColorRestoringContext(tx: tx)
recipient = MessageBackup.RecipientRestoringContext(
localIdentifiers: localIdentifiers,
tx: tx
)
chat = MessageBackup.ChatRestoringContext(
customChatColorContext: customChatColor,
recipientContext: recipient,
tx: tx
)
}
}
let contexts = Contexts(localIdentifiers: localIdentifiers, tx: tx)
while hasMoreFrames {
let frame: BackupProto_Frame
switch stream.readFrame() {
case let .success(_frame, moreBytesAvailable):
frame = _frame
hasMoreFrames = moreBytesAvailable
case .invalidByteLengthDelimiter:
throw OWSAssertionError("invalid byte length delimiter on header")
case .protoDeserializationError(let error):
// fail the whole thing if we fail to deserialize one frame
owsFailDebug("Failed to deserialize proto frame!")
throw error
}
switch frame.item {
case .recipient(let recipient):
let recipientResult: MessageBackup.RestoreFrameResult<MessageBackup.RecipientId>
switch recipient.destination {
case nil:
recipientResult = .failure([.restoreFrameError(
.invalidProtoData(.recipientMissingDestination),
recipient.recipientId
)])
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(_):
// TODO: [Backups] Restore call link recipients.
recipientResult = .failure([.restoreFrameError(.unimplemented, recipient.recipientId)])
}
switch recipientResult {
case .success:
continue
case .partialRestore(let errors):
try processRestoreFrameErrors(errors: errors)
case .failure(let errors):
try processRestoreFrameErrors(errors: errors)
}
case .chat(let chat):
let chatResult = chatArchiver.restore(
chat,
context: contexts.chat
)
switch chatResult {
case .success:
continue
case .partialRestore(let errors):
try processRestoreFrameErrors(errors: errors)
case .failure(let errors):
try processRestoreFrameErrors(errors: errors)
}
case .chatItem(let chatItem):
let chatItemResult = chatItemArchiver.restore(
chatItem,
context: contexts.chat
)
switch chatItemResult {
case .success:
continue
case .partialRestore(let errors):
try processRestoreFrameErrors(errors: errors)
case .failure(let errors):
try processRestoreFrameErrors(errors: errors)
}
case .account(let backupProtoAccountData):
let accountDataResult = accountDataArchiver.restore(
backupProtoAccountData,
context: contexts.customChatColor
)
switch accountDataResult {
case .success:
continue
case .partialRestore(let errors):
try processRestoreFrameErrors(errors: errors)
case .failure(let errors):
try processRestoreFrameErrors(errors: errors)
}
case .stickerPack(let backupProtoStickerPack):
let stickerPackResult = stickerPackArchiver.restore(
backupProtoStickerPack,
context: MessageBackup.RestoringContext(tx: tx)
)
switch stickerPackResult {
case .success:
continue
case .partialRestore(let errors):
try processRestoreFrameErrors(errors: errors)
case .failure(let errors):
try processRestoreFrameErrors(errors: errors)
}
case .adHocCall(let backupProtoAdHocCall):
// TODO: [Backups] Restore ad-hoc calls.
try processRestoreFrameErrors(errors: [.restoreFrameError(
.unimplemented,
MessageBackup.AdHocCallId(
backupProtoAdHocCall.callID,
recipientId: backupProtoAdHocCall.recipientID
)
)])
case nil:
owsFailDebug("Frame missing item!")
try processRestoreFrameErrors(errors: [.restoreFrameError(
.invalidProtoData(.frameMissingItem),
MessageBackup.EmptyFrameId.shared
)])
}
}
stream.closeFileStream()
/// Take any necessary post-frame-restore actions.
try postFrameRestoreActionManager.performPostFrameRestoreActions(
contexts.all.flatMap { $0.postFrameRestoreActions },
tx: tx
)
// Enqueue downloads for all the attachments.
// TODO: [Backups] do this in a separate write transaction after we query the list media
// tier attachments endpoint. We may be missing cdn number info for backup attachments
// that we can retrieve via the list; also we can cancel the download of attachments
// not in the list endpoint's results. We need to make the list endpoint request durably.
// TODO: [Backups] enqueue download of transit tier attachments where backup tier unavailable
// and wasDownloaded=true.
let nowDate = dateProvider()
let shouldDownloadAllFullsize = backupAttachmentDownloadStore.getShouldStoreAllMediaLocally(tx: tx)
try backupAttachmentDownloadStore.dequeueAndClearTable(tx: tx) { backupDownload in
// Every backup attachment gets enqueued for thumbnail download at lower priority.
/*
TODO: Re-enable thumbnail downloading once AttachmentDownloadManager understands thumbnail types
attachmentDownloadManager.enqueueDownloadOfAttachment(
id: backupDownload.attachmentRowId,
priority: .backupRestoreLow,
source: .mediaTierThumbnail,
tx: tx
)
*/
// If its recent media, also download fullsize at higher priority.
// Or if "optimize media" is off, download fullsize everything
// regardless of date.
let isRecentMedia = backupDownload.isRecentMedia(now: nowDate)
if
shouldDownloadAllFullsize
|| isRecentMedia
{
attachmentDownloadManager.enqueueDownloadOfAttachment(
id: backupDownload.attachmentRowId,
priority: isRecentMedia ? .backupRestoreHigh : .backupRestoreLow,
source: .mediaTierFullsize,
tx: tx
)
}
}
}
private func processRestoreFrameErrors<IdType>(errors: [MessageBackup.RestoreFrameError<IdType>]) throws {
MessageBackup.log(errors)
// At time of writing, we want to fail for every single error.
if errors.isEmpty.negated {
throw BackupError()
}
}
// MARK: -
/// TSAttachments must be migrated to v2 Attachments before we can create or restore backups.
/// Normally this migration happens in the background; force it to run and finish now.
private func migrateAttachmentsBeforeBackup() async {
await incrementalTSAttachmentMigrator.runUntilFinished()
}
public func validateEncryptedBackup(
fileUrl: URL,
localIdentifiers: LocalIdentifiers
) async throws {
let key = try db.read { tx in
return try messageBackupKeyMaterial.messageBackupKey(localAci: localIdentifiers.aci, tx: tx)
}
let fileSize = OWSFileSystem.fileSize(ofPath: fileUrl.path)?.uint64Value ?? 0
do {
let result = try validateMessageBackup(key: key, purpose: .remoteBackup, 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:
throw BackupValidationError.validationFailed(
message: validationError.errorMessage,
unknownFields: validationError.unknownFields.fields
)
case SignalError.ioError(let description):
throw BackupValidationError.ioError(description)
default:
throw BackupValidationError.unknownError
}
}
}
}