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
 |