mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			317 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Objective-C
		
	
			
		
		
	
	
			317 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Objective-C
		
	
| //
 | |
| //  Copyright (c) 2018 Open Whisper Systems. All rights reserved.
 | |
| //
 | |
| 
 | |
| #import "OWSBackupJob.h"
 | |
| #import "OWSBackupIO.h"
 | |
| #import "Session-Swift.h"
 | |
| #import <PromiseKit/AnyPromise.h>
 | |
| #import <SignalCoreKit/Randomness.h>
 | |
| #import <YapDatabase/YapDatabaseCryptoUtils.h>
 | |
| 
 | |
| 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<OWSBackupJobDelegate> 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<OWSBackupJobDelegate>)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<NSString *, id> *_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<OWSBackupFragment *> *_Nullable databaseItems =
 | |
|         [self parseManifestItems:json key:kOWSBackup_ManifestKey_DatabaseFiles];
 | |
|     if (!databaseItems) {
 | |
|         return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"No database items in manifest.")];
 | |
|     }
 | |
|     NSArray<OWSBackupFragment *> *_Nullable attachmentsItems =
 | |
|         [self parseManifestItems:json key:kOWSBackup_ManifestKey_AttachmentFiles];
 | |
|     if (!attachmentsItems) {
 | |
|         return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"No attachment items in manifest.")];
 | |
|     }
 | |
| 
 | |
|     NSArray<OWSBackupFragment *> *_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<OWSBackupFragment *> *)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<OWSBackupFragment *> *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
 |