// // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "OWSBackupJob.h" #import "OWSBackupIO.h" #import "Session-Swift.h" #import #import #import NS_ASSUME_NONNULL_BEGIN NSString *const kOWSBackup_ManifestKey_DatabaseFiles = @"database_files"; NSString *const kOWSBackup_ManifestKey_AttachmentFiles = @"attachment_files"; NSString *const kOWSBackup_ManifestKey_RecordName = @"record_name"; NSString *const kOWSBackup_ManifestKey_EncryptionKey = @"encryption_key"; NSString *const kOWSBackup_ManifestKey_RelativeFilePath = @"relative_file_path"; NSString *const kOWSBackup_ManifestKey_AttachmentId = @"attachment_id"; NSString *const kOWSBackup_ManifestKey_DataSize = @"data_size"; NSString *const kOWSBackup_ManifestKey_LocalProfileAvatar = @"local_profile_avatar"; NSString *const kOWSBackup_ManifestKey_LocalProfileName = @"local_profile_name"; NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService"; @implementation OWSBackupManifestContents @end #pragma mark - @interface OWSBackupJob () @property (nonatomic, weak) id delegate; @property (nonatomic) NSString *recipientId; @property (atomic) BOOL isComplete; @property (atomic) BOOL hasSucceeded; @property (nonatomic) NSString *jobTempDirPath; @end #pragma mark - @implementation OWSBackupJob - (instancetype)initWithDelegate:(id)delegate recipientId:(NSString *)recipientId { self = [super init]; if (!self) { return self; } OWSAssertDebug(recipientId.length > 0); OWSAssertDebug([OWSStorage isStorageReady]); self.delegate = delegate; self.recipientId = recipientId; return self; } - (void)dealloc { // Surface memory leaks by logging the deallocation. OWSLogVerbose(@"Dealloc: %@", self.class); [[NSNotificationCenter defaultCenter] removeObserver:self]; if (self.jobTempDirPath) { [OWSFileSystem deleteFileIfExists:self.jobTempDirPath]; } } - (BOOL)ensureJobTempDir { OWSLogVerbose(@""); // TODO: Exports should use a new directory each time, but imports // might want to use a predictable directory so that repeated // import attempts can reuse downloads from previous attempts. NSString *temporaryDirectory = OWSTemporaryDirectory(); self.jobTempDirPath = [temporaryDirectory stringByAppendingPathComponent:[NSUUID UUID].UUIDString]; if (![OWSFileSystem ensureDirectoryExists:self.jobTempDirPath]) { OWSFailDebug(@"Could not create jobTempDirPath."); return NO; } return YES; } #pragma mark - - (void)cancel { OWSAssertIsOnMainThread(); self.isComplete = YES; } - (void)succeed { OWSLogInfo(@""); dispatch_async(dispatch_get_main_queue(), ^{ if (self.isComplete) { OWSAssertDebug(!self.hasSucceeded); return; } self.isComplete = YES; // There's a lot of asynchrony in these backup jobs; // ensure we only end up finishing these jobs once. OWSAssertDebug(!self.hasSucceeded); self.hasSucceeded = YES; [self.delegate backupJobDidSucceed:self]; }); } - (void)failWithErrorDescription:(NSString *)description { [self failWithError:OWSErrorWithCodeDescription(OWSErrorCodeImportBackupFailed, description)]; } - (void)failWithError:(NSError *)error { OWSFailDebug(@"%@", error); dispatch_async(dispatch_get_main_queue(), ^{ OWSAssertDebug(!self.hasSucceeded); if (self.isComplete) { return; } self.isComplete = YES; [self.delegate backupJobDidFail:self error:error]; }); } - (void)updateProgressWithDescription:(nullable NSString *)description progress:(nullable NSNumber *)progress { OWSLogInfo(@""); dispatch_async(dispatch_get_main_queue(), ^{ if (self.isComplete) { return; } [self.delegate backupJobDidUpdate:self description:description progress:progress]; }); } #pragma mark - Manifest - (AnyPromise *)downloadAndProcessManifestWithBackupIO:(OWSBackupIO *)backupIO { OWSAssertDebug(backupIO); OWSLogVerbose(@""); if (self.isComplete) { // Job was aborted. return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup job no longer active.")]; } return [OWSBackupAPI downloadManifestFromCloudObjcWithRecipientId:self.recipientId].thenInBackground(^(NSData *data) { return [self processManifest:data backupIO:backupIO]; }); } - (AnyPromise *)processManifest:(NSData *)manifestDataEncrypted backupIO:(OWSBackupIO *)backupIO { OWSAssertDebug(manifestDataEncrypted.length > 0); OWSAssertDebug(backupIO); if (self.isComplete) { // Job was aborted. return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup job no longer active.")]; } OWSLogVerbose(@""); NSData *_Nullable manifestDataDecrypted = [backupIO decryptDataAsData:manifestDataEncrypted encryptionKey:self.delegate.backupEncryptionKey]; if (!manifestDataDecrypted) { OWSFailDebug(@"Could not decrypt manifest."); return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not decrypt manifest.")]; } NSError *error; NSDictionary *_Nullable json = [NSJSONSerialization JSONObjectWithData:manifestDataDecrypted options:0 error:&error]; if (![json isKindOfClass:[NSDictionary class]]) { OWSFailDebug(@"Could not download manifest."); return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not download manifest.")]; } OWSLogVerbose(@"json: %@", json); NSArray *_Nullable databaseItems = [self parseManifestItems:json key:kOWSBackup_ManifestKey_DatabaseFiles]; if (!databaseItems) { return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"No database items in manifest.")]; } NSArray *_Nullable attachmentsItems = [self parseManifestItems:json key:kOWSBackup_ManifestKey_AttachmentFiles]; if (!attachmentsItems) { return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"No attachment items in manifest.")]; } NSArray *_Nullable localProfileAvatarItems; if ([self parseManifestItem:json key:kOWSBackup_ManifestKey_LocalProfileAvatar]) { localProfileAvatarItems = [self parseManifestItems:json key:kOWSBackup_ManifestKey_LocalProfileAvatar]; } NSString *_Nullable localProfileName = [self parseManifestItem:json key:kOWSBackup_ManifestKey_LocalProfileName]; OWSBackupManifestContents *contents = [OWSBackupManifestContents new]; contents.databaseItems = databaseItems; contents.attachmentsItems = attachmentsItems; contents.localProfileAvatarItem = localProfileAvatarItems.firstObject; if ([localProfileName isKindOfClass:[NSString class]]) { contents.localProfileName = localProfileName; } else if (localProfileName) { OWSFailDebug(@"Invalid localProfileName: %@", [localProfileName class]); } return [AnyPromise promiseWithValue:contents]; } - (nullable id)parseManifestItem:(id)json key:(NSString *)key { OWSAssertDebug(json); OWSAssertDebug(key.length); if (![json isKindOfClass:[NSDictionary class]]) { OWSFailDebug(@"manifest has invalid data."); return nil; } id _Nullable value = json[key]; return value; } - (nullable NSArray *)parseManifestItems:(id)json key:(NSString *)key { OWSAssertDebug(json); OWSAssertDebug(key.length); if (![json isKindOfClass:[NSDictionary class]]) { OWSFailDebug(@"manifest has invalid data."); return nil; } NSArray *itemMaps = json[key]; if (![itemMaps isKindOfClass:[NSArray class]]) { OWSFailDebug(@"manifest has invalid data."); return nil; } NSMutableArray *items = [NSMutableArray new]; for (NSDictionary *itemMap in itemMaps) { if (![itemMap isKindOfClass:[NSDictionary class]]) { OWSFailDebug(@"manifest has invalid item."); return nil; } NSString *_Nullable recordName = itemMap[kOWSBackup_ManifestKey_RecordName]; NSString *_Nullable encryptionKeyString = itemMap[kOWSBackup_ManifestKey_EncryptionKey]; NSString *_Nullable relativeFilePath = itemMap[kOWSBackup_ManifestKey_RelativeFilePath]; NSString *_Nullable attachmentId = itemMap[kOWSBackup_ManifestKey_AttachmentId]; NSNumber *_Nullable uncompressedDataLength = itemMap[kOWSBackup_ManifestKey_DataSize]; if (![recordName isKindOfClass:[NSString class]]) { OWSFailDebug(@"manifest has invalid recordName: %@.", recordName); return nil; } if (![encryptionKeyString isKindOfClass:[NSString class]]) { OWSFailDebug(@"manifest has invalid encryptionKey."); return nil; } // relativeFilePath is an optional field. if (relativeFilePath && ![relativeFilePath isKindOfClass:[NSString class]]) { OWSLogDebug(@"manifest has invalid relativeFilePath: %@.", relativeFilePath); OWSFailDebug(@"manifest has invalid relativeFilePath"); return nil; } // attachmentId is an optional field. if (attachmentId && ![attachmentId isKindOfClass:[NSString class]]) { OWSLogDebug(@"manifest has invalid attachmentId: %@.", attachmentId); OWSFailDebug(@"manifest has invalid attachmentId"); return nil; } NSData *_Nullable encryptionKey = [NSData dataFromBase64String:encryptionKeyString]; if (!encryptionKey) { OWSFailDebug(@"manifest has corrupt encryptionKey"); return nil; } // uncompressedDataLength is an optional field. if (uncompressedDataLength && ![uncompressedDataLength isKindOfClass:[NSNumber class]]) { OWSFailDebug(@"manifest has invalid uncompressedDataLength: %@.", uncompressedDataLength); return nil; } OWSBackupFragment *item = [[OWSBackupFragment alloc] initWithUniqueId:recordName]; item.recordName = recordName; item.encryptionKey = encryptionKey; item.relativeFilePath = relativeFilePath; item.attachmentId = attachmentId; item.uncompressedDataLength = uncompressedDataLength; [items addObject:item]; } return items; } @end NS_ASSUME_NONNULL_END