mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Rework database snapshot representation, encryption, etc.
This commit is contained in:
parent
ca7c75a081
commit
0c81d5656f
|
@ -33,10 +33,20 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
#pragma mark - Decrypt
|
||||
|
||||
- (nullable NSString *)decryptFileAsTempFile:(NSString *)srcFilePath encryptionKey:(NSData *)encryptionKey;
|
||||
- (BOOL)decryptFileAsFile:(NSString *)srcFilePath
|
||||
dstFilePath:(NSString *)dstFilePath
|
||||
encryptionKey:(NSData *)encryptionKey;
|
||||
|
||||
- (nullable NSData *)decryptFileAsData:(NSString *)srcFilePath encryptionKey:(NSData *)encryptionKey;
|
||||
|
||||
- (nullable NSData *)decryptDataAsData:(NSData *)srcData encryptionKey:(NSData *)encryptionKey;
|
||||
|
||||
#pragma mark - Compression
|
||||
|
||||
- (nullable NSData *)compressData:(NSData *)srcData;
|
||||
|
||||
- (nullable NSData *)decompressData:(NSData *)srcData uncompressedDataLength:(NSUInteger)uncompressedDataLength;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
#import <Curve25519Kit/Randomness.h>
|
||||
#import <SignalServiceKit/OWSFileSystem.h>
|
||||
|
||||
@import Compression;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// TODO:
|
||||
|
@ -95,11 +97,41 @@ static const NSUInteger kOWSBackupKeyLength = 32;
|
|||
|
||||
#pragma mark - Decrypt
|
||||
|
||||
- (nullable NSString *)decryptFileAsTempFile:(NSString *)srcFilePath encryptionKey:(NSData *)encryptionKey
|
||||
- (BOOL)decryptFileAsFile:(NSString *)srcFilePath
|
||||
dstFilePath:(NSString *)dstFilePath
|
||||
encryptionKey:(NSData *)encryptionKey
|
||||
{
|
||||
OWSAssert(srcFilePath.length > 0);
|
||||
OWSAssert(encryptionKey.length > 0);
|
||||
|
||||
// TODO: Decrypt the file without loading it into memory.
|
||||
NSData *data = [self decryptFileAsData:srcFilePath encryptionKey:encryptionKey];
|
||||
|
||||
if (!data) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
BOOL success = [data writeToFile:dstFilePath options:NSDataWritingAtomic error:&error];
|
||||
if (!success || error) {
|
||||
OWSProdLogAndFail(@"%@ error writing decrypted data: %@", self.logTag, error);
|
||||
return NO;
|
||||
}
|
||||
[OWSFileSystem protectFileOrFolderAtPath:dstFilePath];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (nullable NSData *)decryptFileAsData:(NSString *)srcFilePath encryptionKey:(NSData *)encryptionKey
|
||||
{
|
||||
OWSAssert(srcFilePath.length > 0);
|
||||
OWSAssert(encryptionKey.length > 0);
|
||||
|
||||
if (![NSFileManager.defaultManager fileExistsAtPath:srcFilePath]) {
|
||||
DDLogError(@"%@ missing downloaded file.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// TODO: Decrypt the file without loading it into memory.
|
||||
NSData *_Nullable srcData = [NSData dataWithContentsOfFile:srcFilePath];
|
||||
if (!srcData) {
|
||||
|
@ -108,20 +140,7 @@ static const NSUInteger kOWSBackupKeyLength = 32;
|
|||
}
|
||||
|
||||
NSData *_Nullable dstData = [self decryptDataAsData:srcData encryptionKey:encryptionKey];
|
||||
if (!dstData) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *dstFilePath = [self.jobTempDirPath stringByAppendingPathComponent:[NSUUID UUID].UUIDString];
|
||||
NSError *error;
|
||||
BOOL success = [dstData writeToFile:dstFilePath options:NSDataWritingAtomic error:&error];
|
||||
if (!success || error) {
|
||||
OWSProdLogAndFail(@"%@ error writing decrypted data: %@", self.logTag, error);
|
||||
return nil;
|
||||
}
|
||||
[OWSFileSystem protectFileOrFolderAtPath:dstFilePath];
|
||||
|
||||
return dstFilePath;
|
||||
return dstData;
|
||||
}
|
||||
|
||||
- (nullable NSData *)decryptDataAsData:(NSData *)srcData encryptionKey:(NSData *)encryptionKey
|
||||
|
@ -134,6 +153,75 @@ static const NSUInteger kOWSBackupKeyLength = 32;
|
|||
return srcData;
|
||||
}
|
||||
|
||||
#pragma mark - Compression
|
||||
|
||||
- (nullable NSData *)compressData:(NSData *)srcData
|
||||
{
|
||||
OWSAssert(srcData);
|
||||
|
||||
if (!srcData) {
|
||||
OWSProdLogAndFail(@"%@ missing unencrypted data.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
size_t srcLength = [srcData length];
|
||||
const uint8_t *srcBuffer = (const uint8_t *)[srcData bytes];
|
||||
if (!srcBuffer) {
|
||||
return nil;
|
||||
}
|
||||
// This assumes that dst will always be smaller than src.
|
||||
//
|
||||
// We slightly pad the buffer size to account for the worst case.
|
||||
uint8_t *dstBuffer = malloc(sizeof(uint8_t) * srcLength) + 64 * 1024;
|
||||
if (!dstBuffer) {
|
||||
return nil;
|
||||
}
|
||||
// TODO: Should we use COMPRESSION_LZFSE?
|
||||
size_t dstLength = compression_encode_buffer(dstBuffer, srcLength, srcBuffer, srcLength, NULL, COMPRESSION_LZFSE);
|
||||
NSData *compressedData = [NSData dataWithBytes:dstBuffer length:dstLength];
|
||||
DDLogVerbose(@"%@ compressed %zd -> %zd = %0.2f",
|
||||
self.logTag,
|
||||
srcLength,
|
||||
dstLength,
|
||||
(srcLength > 0 ? (dstLength / (CGFloat)srcLength) : 0));
|
||||
free(dstBuffer);
|
||||
|
||||
return compressedData;
|
||||
}
|
||||
|
||||
- (nullable NSData *)decompressData:(NSData *)srcData uncompressedDataLength:(NSUInteger)uncompressedDataLength
|
||||
{
|
||||
OWSAssert(srcData);
|
||||
|
||||
if (!srcData) {
|
||||
OWSProdLogAndFail(@"%@ missing unencrypted data.", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
size_t srcLength = [srcData length];
|
||||
const uint8_t *srcBuffer = (const uint8_t *)[srcData bytes];
|
||||
if (!srcBuffer) {
|
||||
return nil;
|
||||
}
|
||||
// We pad the buffer to be defensive.
|
||||
uint8_t *dstBuffer = malloc(sizeof(uint8_t) * (uncompressedDataLength + 1024));
|
||||
if (!dstBuffer) {
|
||||
return nil;
|
||||
}
|
||||
// TODO: Should we use COMPRESSION_LZFSE?
|
||||
size_t dstLength = compression_decode_buffer(dstBuffer, srcLength, srcBuffer, srcLength, NULL, COMPRESSION_LZFSE);
|
||||
NSData *decompressedData = [NSData dataWithBytes:dstBuffer length:dstLength];
|
||||
OWSAssert(decompressedData.length == uncompressedDataLength);
|
||||
DDLogVerbose(@"%@ decompressed %zd -> %zd = %0.2f",
|
||||
self.logTag,
|
||||
srcLength,
|
||||
dstLength,
|
||||
(dstLength > 0 ? (srcLength / (CGFloat)dstLength) : 0));
|
||||
free(dstBuffer);
|
||||
|
||||
return decompressedData;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
//
|
||||
|
||||
#import "OWSBackupExportJob.h"
|
||||
#import "OWSBackupEncryption.h"
|
||||
#import "OWSDatabaseMigration.h"
|
||||
#import "OWSSignalServiceProtos.pb.h"
|
||||
#import "Signal-Swift.h"
|
||||
#import <SignalServiceKit/NSData+Base64.h>
|
||||
#import <SignalServiceKit/NSDate+OWS.h>
|
||||
#import <SignalServiceKit/OWSBackgroundTask.h>
|
||||
#import <SignalServiceKit/OWSBackupStorage.h>
|
||||
#import <SignalServiceKit/OWSError.h>
|
||||
#import <SignalServiceKit/OWSFileSystem.h>
|
||||
#import <SignalServiceKit/TSAttachment.h>
|
||||
|
@ -18,11 +18,6 @@
|
|||
#import <SignalServiceKit/TSThread.h>
|
||||
#import <SignalServiceKit/Threading.h>
|
||||
|
||||
@import Compression;
|
||||
#import "OWSBackupEncryption.h"
|
||||
|
||||
// TODO:
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class OWSAttachmentExport;
|
||||
|
@ -40,6 +35,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
// This property is optional and is only used for attachments.
|
||||
@property (nonatomic, nullable) OWSAttachmentExport *attachmentExport;
|
||||
|
||||
// This property is optional.
|
||||
@property (nonatomic, nullable) NSNumber *uncompressedDataLength;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
|
@ -69,7 +67,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
@property (nonatomic) OWSBackupEncryption *encryption;
|
||||
|
||||
@property (nonatomic) NSMutableArray<OWSBackupEncryptedItem *> *encryptedItems;
|
||||
@property (nonatomic) NSMutableArray<OWSBackupExportItem *> *exportItems;
|
||||
|
||||
@property (nonatomic, nullable) OWSSignalServiceProtosBackupSnapshotBuilder *backupSnapshotBuilder;
|
||||
|
||||
|
@ -93,7 +91,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
OWSAssert(encryption);
|
||||
|
||||
self.encryptedItems = [NSMutableArray new];
|
||||
self.exportItems = [NSMutableArray new];
|
||||
self.encryption = encryption;
|
||||
|
||||
return self;
|
||||
|
@ -117,7 +115,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSSignalServiceProtosBackupSnapshotBackupEntityBuilder *entityBuilder =
|
||||
[OWSSignalServiceProtosBackupSnapshotBackupEntityBuilder new];
|
||||
[entityBuilder setType:entityType];
|
||||
[entityBuilder setData:data];
|
||||
[entityBuilder setEntityData:data];
|
||||
|
||||
[self.backupSnapshotBuilder addEntity:[entityBuilder build]];
|
||||
|
||||
|
@ -140,6 +138,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
|
||||
NSData *_Nullable uncompressedData = [self.backupSnapshotBuilder build].data;
|
||||
NSUInteger uncompressedDataLength = uncompressedData.length;
|
||||
self.backupSnapshotBuilder = nil;
|
||||
self.cachedItemCount = 0;
|
||||
if (!uncompressedData) {
|
||||
|
@ -147,31 +146,18 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return NO;
|
||||
}
|
||||
|
||||
size_t srcLength = [uncompressedData length];
|
||||
const uint8_t *srcBuffer = (const uint8_t *)[uncompressedData bytes];
|
||||
if (!srcBuffer) {
|
||||
return NO;
|
||||
}
|
||||
// This assumes that dst will always be smaller than src.
|
||||
uint8_t *dstBuffer = malloc(sizeof(uint8_t) * srcLength);
|
||||
if (!dstBuffer) {
|
||||
return NO;
|
||||
}
|
||||
// TODO: Should we use COMPRESSION_LZFSE?
|
||||
size_t dstLength = compression_encode_buffer(dstBuffer, srcLength, srcBuffer, srcLength, NULL, COMPRESSION_LZFSE);
|
||||
NSData *compressedData = [NSData dataWithBytes:dstBuffer length:dstLength];
|
||||
DDLogVerbose(@"%@ compressed %zd -> %zd = %0.2f",
|
||||
self.logTag,
|
||||
srcLength,
|
||||
dstLength,
|
||||
(dstLength > srcLength ? (dstLength / (CGFloat)srcLength) : 0));
|
||||
free(dstBuffer);
|
||||
NSData *compressedData = [self.encryption compressData:uncompressedData];
|
||||
|
||||
OWSBackupEncryptedItem *_Nullable encryptedItem = [self.encryption encryptDataAsTempFile:compressedData];
|
||||
if (!encryptedItem) {
|
||||
OWSProdLogAndFail(@"%@ couldn't encrypt database snapshot.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
|
||||
OWSBackupExportItem *exportItem = [[OWSBackupExportItem alloc] initWithEncryptedItem:encryptedItem];
|
||||
exportItem.uncompressedDataLength = @(uncompressedDataLength);
|
||||
[self.exportItems addObject:exportItem];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
@ -519,8 +505,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return NO;
|
||||
}
|
||||
|
||||
self.unsavedDatabaseItems =
|
||||
[[self exportItemsForEncryptedItems:exportStream.encryptedItems label:@"database snapshots"] mutableCopy];
|
||||
self.unsavedDatabaseItems = [exportStream.exportItems mutableCopy];
|
||||
|
||||
// TODO: Should we do a database checkpoint?
|
||||
|
||||
|
@ -533,26 +518,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return YES;
|
||||
}
|
||||
|
||||
- (NSArray<OWSBackupExportItem *> *)exportItemsForEncryptedItems:(NSArray<OWSBackupEncryptedItem *> *)encryptedItems
|
||||
label:(NSString *)label
|
||||
{
|
||||
OWSAssert(encryptedItems);
|
||||
OWSAssert(label.length);
|
||||
|
||||
NSMutableArray<OWSBackupExportItem *> *exportItems = [NSMutableArray new];
|
||||
unsigned long long totalFileSize = 0;
|
||||
for (OWSBackupEncryptedItem *encryptedItem in encryptedItems) {
|
||||
OWSBackupExportItem *exportItem = [OWSBackupExportItem new];
|
||||
exportItem.encryptedItem = encryptedItem;
|
||||
[exportItems addObject:exportItem];
|
||||
|
||||
totalFileSize += [OWSFileSystem fileSizeOfPath:encryptedItem.filePath].unsignedLongLongValue;
|
||||
}
|
||||
DDLogInfo(@"%@ exported %@: count: %zd, bytes: %llu.", self.logTag, label, exportItems.count, totalFileSize);
|
||||
|
||||
return exportItems;
|
||||
}
|
||||
|
||||
- (void)saveToCloud:(OWSBackupJobCompletion)completion
|
||||
{
|
||||
OWSAssert(completion);
|
||||
|
@ -562,6 +527,29 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
self.savedDatabaseItems = [NSMutableArray new];
|
||||
self.savedAttachmentItems = [NSMutableArray new];
|
||||
|
||||
{
|
||||
unsigned long long totalFileSize = 0;
|
||||
for (OWSBackupExportItem *item in self.unsavedDatabaseItems) {
|
||||
totalFileSize += [OWSFileSystem fileSizeOfPath:item.encryptedItem.filePath].unsignedLongLongValue;
|
||||
}
|
||||
DDLogInfo(@"%@ exporting %@: count: %zd, bytes: %llu.",
|
||||
self.logTag,
|
||||
@"database items",
|
||||
self.unsavedDatabaseItems.count,
|
||||
totalFileSize);
|
||||
}
|
||||
{
|
||||
unsigned long long totalFileSize = 0;
|
||||
for (OWSAttachmentExport *attachmentExport in self.unsavedAttachmentExports) {
|
||||
totalFileSize += [OWSFileSystem fileSizeOfPath:attachmentExport.attachmentFilePath].unsignedLongLongValue;
|
||||
}
|
||||
DDLogInfo(@"%@ exporting %@: count: %zd, bytes: %llu.",
|
||||
self.logTag,
|
||||
@"attachment items",
|
||||
self.unsavedAttachmentExports.count,
|
||||
totalFileSize);
|
||||
}
|
||||
|
||||
[self saveNextFileToCloud:completion];
|
||||
}
|
||||
|
||||
|
@ -773,6 +761,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSAssert(item.attachmentExport.relativeFilePath.length > 0);
|
||||
itemJson[kOWSBackup_ManifestKey_RelativeFilePath] = item.attachmentExport.relativeFilePath;
|
||||
}
|
||||
if (item.uncompressedDataLength) {
|
||||
itemJson[kOWSBackup_ManifestKey_DataSize] = item.uncompressedDataLength;
|
||||
}
|
||||
[result addObject:itemJson];
|
||||
}
|
||||
|
||||
|
@ -852,8 +843,12 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
if (obsoleteRecordNames.count < 1) {
|
||||
// No more records to delete; cleanup is complete.
|
||||
completion(nil);
|
||||
return;
|
||||
return completion(nil);
|
||||
}
|
||||
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return completion(nil);
|
||||
}
|
||||
|
||||
CGFloat progress = (obsoleteRecordNames.count / (CGFloat)(obsoleteRecordNames.count + deletedCount));
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
#import "Signal-Swift.h"
|
||||
#import <SignalServiceKit/NSData+Base64.h>
|
||||
#import <SignalServiceKit/OWSBackgroundTask.h>
|
||||
#import <SignalServiceKit/OWSBackupStorage.h>
|
||||
#import <SignalServiceKit/OWSFileSystem.h>
|
||||
#import <SignalServiceKit/TSAttachment.h>
|
||||
#import <SignalServiceKit/TSMessage.h>
|
||||
|
@ -21,20 +20,36 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
|
|||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSBackupImportItem : NSObject
|
||||
|
||||
@property (nonatomic) NSString *recordName;
|
||||
|
||||
@property (nonatomic) NSData *encryptionKey;
|
||||
|
||||
@property (nonatomic, nullable) NSString *relativeFilePath;
|
||||
|
||||
@property (nonatomic, nullable) NSString *downloadFilePath;
|
||||
|
||||
@property (nonatomic, nullable) NSNumber *uncompressedDataLength;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSBackupImportItem
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSBackupImportJob ()
|
||||
|
||||
@property (nonatomic, nullable) OWSBackgroundTask *backgroundTask;
|
||||
|
||||
@property (nonatomic, nullable) OWSBackupStorage *backupStorage;
|
||||
@property (nonatomic) OWSBackupEncryption *encryption;
|
||||
|
||||
// A map of "record name"-to-"file name".
|
||||
@property (nonatomic) NSMutableDictionary<NSString *, NSString *> *databaseRecordMap;
|
||||
|
||||
// A map of "record name"-to-"file relative path".
|
||||
@property (nonatomic) NSMutableDictionary<NSString *, NSString *> *attachmentRecordMap;
|
||||
|
||||
// A map of "record name"-to-"downloaded file path".
|
||||
@property (nonatomic) NSMutableDictionary<NSString *, NSString *> *downloadedFileMap;
|
||||
@property (nonatomic) NSArray<OWSBackupImportItem *> *databaseItems;
|
||||
@property (nonatomic) NSArray<OWSBackupImportItem *> *attachmentsItems;
|
||||
|
||||
@end
|
||||
|
||||
|
@ -93,13 +108,13 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
|
|||
return;
|
||||
}
|
||||
|
||||
NSMutableArray<NSString *> *allRecordNames = [NSMutableArray new];
|
||||
[allRecordNames addObjectsFromArray:weakSelf.databaseRecordMap.allKeys];
|
||||
// TODO: We could skip attachments that have already been restored
|
||||
// by previous "backup import" attempts.
|
||||
[allRecordNames addObjectsFromArray:weakSelf.attachmentRecordMap.allKeys];
|
||||
OWSAssert(self.databaseItems);
|
||||
OWSAssert(self.attachmentsItems);
|
||||
NSMutableArray<OWSBackupImportItem *> *allItems = [NSMutableArray new];
|
||||
[allItems addObjectsFromArray:self.databaseItems];
|
||||
[allItems addObjectsFromArray:self.attachmentsItems];
|
||||
[weakSelf
|
||||
downloadFilesFromCloud:allRecordNames
|
||||
downloadFilesFromCloud:allItems
|
||||
completion:^(NSError *_Nullable fileDownloadError) {
|
||||
if (fileDownloadError) {
|
||||
[weakSelf failWithError:fileDownloadError];
|
||||
|
@ -157,6 +172,9 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
|
|||
OWSProdLogAndFail(@"%@ Could not create jobTempDirPath.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
|
||||
self.encryption = [[OWSBackupEncryption alloc] initWithJobTempDirPath:self.jobTempDirPath];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
@ -183,14 +201,15 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
|
|||
}
|
||||
failure:^(NSError *error) {
|
||||
// The manifest file is critical so any error downloading it is unrecoverable.
|
||||
OWSProdLogAndFail(@"%@ Could not download manifest.", self.logTag);
|
||||
OWSProdLogAndFail(@"%@ Could not download manifest.", weakSelf.logTag);
|
||||
completion(error);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)processManifest:(NSData *)manifestData completion:(OWSBackupJobBoolCompletion)completion
|
||||
- (void)processManifest:(NSData *)manifestDataEncrypted completion:(OWSBackupJobBoolCompletion)completion
|
||||
{
|
||||
OWSAssert(completion);
|
||||
OWSAssert(self.encryption);
|
||||
|
||||
if (self.isComplete) {
|
||||
return;
|
||||
|
@ -198,9 +217,16 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
|
|||
|
||||
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
NSData *_Nullable manifestDataDecrypted =
|
||||
[self.encryption decryptDataAsData:manifestDataEncrypted encryptionKey:self.delegate.backupEncryptionKey];
|
||||
if (!manifestDataDecrypted) {
|
||||
OWSProdLogAndFail(@"%@ Could not decrypt manifest.", self.logTag);
|
||||
return completion(NO);
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
NSDictionary<NSString *, id> *_Nullable json =
|
||||
[NSJSONSerialization JSONObjectWithData:manifestData options:0 error:&error];
|
||||
[NSJSONSerialization JSONObjectWithData:manifestDataDecrypted options:0 error:&error];
|
||||
if (![json isKindOfClass:[NSDictionary class]]) {
|
||||
OWSProdLogAndFail(@"%@ Could not download manifest.", self.logTag);
|
||||
return completion(NO);
|
||||
|
@ -208,49 +234,97 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
|
|||
|
||||
DDLogVerbose(@"%@ json: %@", self.logTag, json);
|
||||
|
||||
NSDictionary<NSString *, NSString *> *_Nullable databaseRecordMap = json[kOWSBackup_ManifestKey_DatabaseFiles];
|
||||
NSDictionary<NSString *, NSString *> *_Nullable attachmentRecordMap = json[kOWSBackup_ManifestKey_AttachmentFiles];
|
||||
NSString *_Nullable databaseKeySpecBase64 = json[kOWSBackup_ManifestKey_DatabaseKeySpec];
|
||||
if (!([databaseRecordMap isKindOfClass:[NSDictionary class]] &&
|
||||
[attachmentRecordMap isKindOfClass:[NSDictionary class]] &&
|
||||
[databaseKeySpecBase64 isKindOfClass:[NSString class]])) {
|
||||
OWSProdLogAndFail(@"%@ Invalid manifest.", self.logTag);
|
||||
NSArray<OWSBackupImportItem *> *_Nullable databaseItems =
|
||||
[self parseItems:json key:kOWSBackup_ManifestKey_DatabaseFiles];
|
||||
if (!databaseItems) {
|
||||
return completion(NO);
|
||||
}
|
||||
NSData *_Nullable databaseKeySpec = [NSData dataFromBase64String:databaseKeySpecBase64];
|
||||
if (!databaseKeySpec) {
|
||||
OWSProdLogAndFail(@"%@ Invalid manifest databaseKeySpec.", self.logTag);
|
||||
return completion(NO);
|
||||
}
|
||||
if (![OWSBackupJob storeDatabaseKeySpec:databaseKeySpec keychainKey:kOWSBackup_ImportDatabaseKeySpec]) {
|
||||
OWSProdLogAndFail(@"%@ Couldn't store databaseKeySpec from manifest.", self.logTag);
|
||||
NSArray<OWSBackupImportItem *> *_Nullable attachmentsItems =
|
||||
[self parseItems:json key:kOWSBackup_ManifestKey_AttachmentFiles];
|
||||
if (!attachmentsItems) {
|
||||
return completion(NO);
|
||||
}
|
||||
|
||||
self.databaseRecordMap = [databaseRecordMap mutableCopy];
|
||||
self.attachmentRecordMap = [attachmentRecordMap mutableCopy];
|
||||
self.databaseItems = databaseItems;
|
||||
self.attachmentsItems = attachmentsItems;
|
||||
|
||||
return completion(YES);
|
||||
}
|
||||
|
||||
- (void)downloadFilesFromCloud:(NSMutableArray<NSString *> *)recordNames completion:(OWSBackupJobCompletion)completion
|
||||
- (nullable NSArray<OWSBackupImportItem *> *)parseItems:(id)json key:(NSString *)key
|
||||
{
|
||||
OWSAssert(recordNames.count > 0);
|
||||
OWSAssert(json);
|
||||
OWSAssert(key.length);
|
||||
|
||||
if (![json isKindOfClass:[NSArray class]]) {
|
||||
OWSProdLogAndFail(@"%@ manifest has invalid data: %@.", self.logTag, key);
|
||||
return nil;
|
||||
}
|
||||
NSArray *itemMaps = json[key];
|
||||
if (![itemMaps isKindOfClass:[NSArray class]]) {
|
||||
OWSProdLogAndFail(@"%@ manifest has invalid data: %@.", self.logTag, key);
|
||||
return nil;
|
||||
}
|
||||
NSMutableArray<OWSBackupImportItem *> *items = [NSMutableArray new];
|
||||
for (NSDictionary *itemMap in itemMaps) {
|
||||
if (![itemMap isKindOfClass:[NSDictionary class]]) {
|
||||
OWSProdLogAndFail(@"%@ manifest has invalid item: %@.", self.logTag, key);
|
||||
return nil;
|
||||
}
|
||||
NSString *_Nullable recordName = itemMap[kOWSBackup_ManifestKey_RecordName];
|
||||
NSString *_Nullable encryptionKeyString = itemMap[kOWSBackup_ManifestKey_EncryptionKey];
|
||||
NSString *_Nullable relativeFilePath = itemMap[kOWSBackup_ManifestKey_RelativeFilePath];
|
||||
NSNumber *_Nullable uncompressedDataLength = itemMap[kOWSBackup_ManifestKey_DataSize];
|
||||
if (![recordName isKindOfClass:[NSString class]]) {
|
||||
OWSProdLogAndFail(@"%@ manifest has invalid recordName: %@.", self.logTag, key);
|
||||
return nil;
|
||||
}
|
||||
if (![encryptionKeyString isKindOfClass:[NSString class]]) {
|
||||
OWSProdLogAndFail(@"%@ manifest has invalid encryptionKey: %@.", self.logTag, key);
|
||||
return nil;
|
||||
}
|
||||
// relativeFilePath is an optional field.
|
||||
if (relativeFilePath && ![relativeFilePath isKindOfClass:[NSString class]]) {
|
||||
OWSProdLogAndFail(@"%@ manifest has invalid relativeFilePath: %@.", self.logTag, key);
|
||||
return nil;
|
||||
}
|
||||
NSData *_Nullable encryptionKey = [NSData dataFromBase64String:encryptionKeyString];
|
||||
if (!encryptionKey) {
|
||||
OWSProdLogAndFail(@"%@ manifest has corrupt encryptionKey: %@.", self.logTag, key);
|
||||
return nil;
|
||||
}
|
||||
// uncompressedDataLength is an optional field.
|
||||
if (uncompressedDataLength && ![uncompressedDataLength isKindOfClass:[NSNumber class]]) {
|
||||
OWSProdLogAndFail(@"%@ manifest has invalid uncompressedDataLength: %@.", self.logTag, key);
|
||||
return nil;
|
||||
}
|
||||
|
||||
OWSBackupImportItem *item = [OWSBackupImportItem new];
|
||||
item.recordName = recordName;
|
||||
item.encryptionKey = encryptionKey;
|
||||
item.relativeFilePath = relativeFilePath;
|
||||
item.uncompressedDataLength = uncompressedDataLength;
|
||||
[items addObject:item];
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
- (void)downloadFilesFromCloud:(NSMutableArray<OWSBackupImportItem *> *)items
|
||||
completion:(OWSBackupJobCompletion)completion
|
||||
{
|
||||
OWSAssert(items.count > 0);
|
||||
OWSAssert(completion);
|
||||
|
||||
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
// A map of "record name"-to-"downloaded file path".
|
||||
self.downloadedFileMap = [NSMutableDictionary new];
|
||||
|
||||
[self downloadNextFileFromCloud:recordNames recordCount:recordNames.count completion:completion];
|
||||
[self downloadNextItemFromCloud:items recordCount:items.count completion:completion];
|
||||
}
|
||||
|
||||
- (void)downloadNextFileFromCloud:(NSMutableArray<NSString *> *)recordNames
|
||||
- (void)downloadNextItemFromCloud:(NSMutableArray<OWSBackupImportItem *> *)items
|
||||
recordCount:(NSUInteger)recordCount
|
||||
completion:(OWSBackupJobCompletion)completion
|
||||
{
|
||||
OWSAssert(recordNames);
|
||||
OWSAssert(items);
|
||||
OWSAssert(completion);
|
||||
|
||||
if (self.isComplete) {
|
||||
|
@ -258,43 +332,43 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
|
|||
return completion(nil);
|
||||
}
|
||||
|
||||
if (recordNames.count < 1) {
|
||||
if (items.count < 1) {
|
||||
// All downloads are complete; exit.
|
||||
return completion(nil);
|
||||
}
|
||||
NSString *recordName = recordNames.lastObject;
|
||||
[recordNames removeLastObject];
|
||||
OWSBackupImportItem *item = items.lastObject;
|
||||
[items removeLastObject];
|
||||
|
||||
CGFloat progress = (recordCount > 0 ? ((recordCount - items.count) / (CGFloat)recordCount) : 0.f);
|
||||
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_DOWNLOAD",
|
||||
@"Indicates that the backup import data is being downloaded.")
|
||||
progress:@((recordCount - recordNames.count) / (CGFloat)recordCount)];
|
||||
|
||||
if (![recordName isKindOfClass:[NSString class]]) {
|
||||
DDLogError(@"%@ invalid record name in manifest: %@", self.logTag, [recordName class]);
|
||||
// Invalid record name in the manifest. This may be recoverable.
|
||||
// Ignore this for now and proceed with the other downloads.
|
||||
return [self downloadNextFileFromCloud:recordNames recordCount:recordCount completion:completion];
|
||||
}
|
||||
progress:@(progress)];
|
||||
|
||||
// Use a predictable file path so that multiple "import backup" attempts
|
||||
// will leverage successful file downloads from previous attempts.
|
||||
NSString *tempFilePath = [self.jobTempDirPath stringByAppendingPathComponent:recordName];
|
||||
//
|
||||
// TODO: This will also require imports using a predictable jobTempDirPath.
|
||||
NSString *tempFilePath = [self.jobTempDirPath stringByAppendingPathComponent:item.recordName];
|
||||
|
||||
// Skip redundant file download.
|
||||
if ([NSFileManager.defaultManager fileExistsAtPath:tempFilePath]) {
|
||||
[OWSFileSystem protectFileOrFolderAtPath:tempFilePath];
|
||||
self.downloadedFileMap[recordName] = tempFilePath;
|
||||
[self downloadNextFileFromCloud:recordNames recordCount:recordCount completion:completion];
|
||||
|
||||
item.downloadFilePath = tempFilePath;
|
||||
|
||||
[self downloadNextItemFromCloud:items recordCount:recordCount completion:completion];
|
||||
return;
|
||||
}
|
||||
|
||||
[OWSBackupAPI downloadFileFromCloudWithRecordName:recordName
|
||||
__weak OWSBackupImportJob *weakSelf = self;
|
||||
[OWSBackupAPI downloadFileFromCloudWithRecordName:item.recordName
|
||||
toFileUrl:[NSURL fileURLWithPath:tempFilePath]
|
||||
success:^{
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[OWSFileSystem protectFileOrFolderAtPath:tempFilePath];
|
||||
self.downloadedFileMap[recordName] = tempFilePath;
|
||||
[self downloadNextFileFromCloud:recordNames recordCount:recordCount completion:completion];
|
||||
item.downloadFilePath = tempFilePath;
|
||||
|
||||
[weakSelf downloadNextItemFromCloud:items recordCount:recordCount completion:completion];
|
||||
});
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
|
@ -304,28 +378,45 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
|
|||
|
||||
- (void)restoreAttachmentFiles
|
||||
{
|
||||
DDLogVerbose(@"%@ %s: %zd", self.logTag, __PRETTY_FUNCTION__, self.attachmentRecordMap.count);
|
||||
DDLogVerbose(@"%@ %s: %zd", self.logTag, __PRETTY_FUNCTION__, self.attachmentsItems.count);
|
||||
|
||||
NSString *attachmentsDirPath = [TSAttachmentStream attachmentsFolder];
|
||||
|
||||
NSUInteger count = 0;
|
||||
for (NSString *recordName in self.attachmentRecordMap) {
|
||||
for (OWSBackupImportItem *item in self.attachmentsItems) {
|
||||
if (self.isComplete) {
|
||||
return;
|
||||
}
|
||||
if (item.recordName.length < 1) {
|
||||
DDLogError(@"%@ attachment was not downloaded.", self.logTag);
|
||||
// Attachment-related errors are recoverable and can be ignored.
|
||||
continue;
|
||||
}
|
||||
if (item.relativeFilePath.length < 1) {
|
||||
DDLogError(@"%@ attachment missing relative file path.", self.logTag);
|
||||
// Attachment-related errors are recoverable and can be ignored.
|
||||
continue;
|
||||
}
|
||||
|
||||
count++;
|
||||
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_RESTORING_FILES",
|
||||
@"Indicates that the backup import data is being restored.")
|
||||
progress:@(count / (CGFloat)self.attachmentRecordMap.count)];
|
||||
progress:@(count / (CGFloat)self.attachmentsItems.count)];
|
||||
|
||||
|
||||
NSString *dstRelativePath = self.attachmentRecordMap[recordName];
|
||||
if (!
|
||||
[self restoreFileWithRecordName:recordName dstRelativePath:dstRelativePath dstDirPath:attachmentsDirPath]) {
|
||||
NSString *dstFilePath = [attachmentsDirPath stringByAppendingPathComponent:item.relativeFilePath];
|
||||
if ([NSFileManager.defaultManager fileExistsAtPath:dstFilePath]) {
|
||||
DDLogError(@"%@ skipping redundant file restore: %@.", self.logTag, dstFilePath);
|
||||
continue;
|
||||
}
|
||||
if (![self.encryption decryptFileAsFile:item.downloadFilePath
|
||||
dstFilePath:dstFilePath
|
||||
encryptionKey:item.encryptionKey]) {
|
||||
DDLogError(@"%@ attachment could not be restored.", self.logTag);
|
||||
// Attachment-related errors are recoverable and can be ignored.
|
||||
continue;
|
||||
}
|
||||
|
||||
DDLogError(@"%@ restored file: %@.", self.logTag, item.relativeFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -335,84 +426,16 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
|
|||
|
||||
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_RESTORING_DATABASE",
|
||||
@"Indicates that the backup database is being restored.")
|
||||
progress:nil];
|
||||
|
||||
NSString *jobDatabaseDirPath = [self.jobTempDirPath stringByAppendingPathComponent:@"database"];
|
||||
if (![OWSFileSystem ensureDirectoryExists:jobDatabaseDirPath]) {
|
||||
OWSProdLogAndFail(@"%@ Could not create jobDatabaseDirPath.", self.logTag);
|
||||
return completion(NO);
|
||||
}
|
||||
|
||||
for (NSString *recordName in self.databaseRecordMap) {
|
||||
if (self.isComplete) {
|
||||
return completion(NO);
|
||||
}
|
||||
|
||||
NSString *dstRelativePath = self.databaseRecordMap[recordName];
|
||||
if (!
|
||||
[self restoreFileWithRecordName:recordName dstRelativePath:dstRelativePath dstDirPath:jobDatabaseDirPath]) {
|
||||
// Database-related errors are unrecoverable.
|
||||
return completion(NO);
|
||||
}
|
||||
}
|
||||
|
||||
BackupStorageKeySpecBlock keySpecBlock = ^{
|
||||
NSData *_Nullable databaseKeySpec =
|
||||
[OWSBackupJob loadDatabaseKeySpecWithKeychainKey:kOWSBackup_ImportDatabaseKeySpec];
|
||||
if (!databaseKeySpec) {
|
||||
OWSProdLogAndFail(@"%@ Could not load database keyspec for import.", self.logTag);
|
||||
}
|
||||
return databaseKeySpec;
|
||||
};
|
||||
self.backupStorage =
|
||||
[[OWSBackupStorage alloc] initBackupStorageWithDatabaseDirPath:jobDatabaseDirPath keySpecBlock:keySpecBlock];
|
||||
if (!self.backupStorage) {
|
||||
OWSProdLogAndFail(@"%@ Could not create backupStorage.", self.logTag);
|
||||
return completion(NO);
|
||||
}
|
||||
|
||||
// TODO: Do we really need to run these registrations on the main thread?
|
||||
__weak OWSBackupImportJob *weakSelf = self;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[weakSelf.backupStorage runSyncRegistrations];
|
||||
[weakSelf.backupStorage runAsyncRegistrationsWithCompletion:^{
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[weakSelf restoreDatabaseContents:completion];
|
||||
});
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)restoreDatabaseContents:(OWSBackupJobBoolCompletion)completion
|
||||
{
|
||||
OWSAssert(self.backupStorage);
|
||||
OWSAssert(completion);
|
||||
|
||||
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
if (self.isComplete) {
|
||||
return completion(NO);
|
||||
}
|
||||
|
||||
YapDatabaseConnection *_Nullable tempDBConnection = self.backupStorage.newDatabaseConnection;
|
||||
if (!tempDBConnection) {
|
||||
OWSProdLogAndFail(@"%@ Could not create tempDBConnection.", self.logTag);
|
||||
return completion(NO);
|
||||
}
|
||||
YapDatabaseConnection *_Nullable primaryDBConnection = self.primaryStorage.newDatabaseConnection;
|
||||
if (!primaryDBConnection) {
|
||||
OWSProdLogAndFail(@"%@ Could not create primaryDBConnection.", self.logTag);
|
||||
YapDatabaseConnection *_Nullable dbConnection = self.primaryStorage.newDatabaseConnection;
|
||||
if (!dbConnection) {
|
||||
OWSProdLogAndFail(@"%@ Could not create dbConnection.", self.logTag);
|
||||
return completion(NO);
|
||||
}
|
||||
|
||||
NSDictionary<NSString *, Class> *collectionTypeMap = @{
|
||||
[TSThread collection] : [TSThread class],
|
||||
[TSAttachment collection] : [TSAttachment class],
|
||||
[TSInteraction collection] : [TSInteraction class],
|
||||
[OWSDatabaseMigration collection] : [OWSDatabaseMigration class],
|
||||
};
|
||||
// Order matters here.
|
||||
NSArray<NSString *> *collectionsToRestore = @[
|
||||
[TSThread collection],
|
||||
|
@ -425,85 +448,123 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
|
|||
NSMutableDictionary<NSString *, NSNumber *> *restoredEntityCounts = [NSMutableDictionary new];
|
||||
__block unsigned long long copiedEntities = 0;
|
||||
__block BOOL aborted = NO;
|
||||
[tempDBConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *srcTransaction) {
|
||||
[primaryDBConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *dstTransaction) {
|
||||
if (![srcTransaction boolForKey:kOWSBackup_Snapshot_ValidKey
|
||||
inCollection:kOWSBackup_Snapshot_Collection
|
||||
defaultValue:NO]) {
|
||||
DDLogError(@"%@ invalid database.", self.logTag);
|
||||
[dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
for (NSString *collection in collectionsToRestore) {
|
||||
if ([collection isEqualToString:[OWSDatabaseMigration collection]]) {
|
||||
// It's okay if there are existing migrations; we'll clear those
|
||||
// before restoring.
|
||||
continue;
|
||||
}
|
||||
if ([transaction numberOfKeysInCollection:collection] > 0) {
|
||||
DDLogError(@"%@ unexpected contents in database (%@).", self.logTag, collection);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear existing database contents.
|
||||
//
|
||||
// This should be safe since we only ever import into an empty database.
|
||||
//
|
||||
// Note that if the app receives a message after registering and before restoring
|
||||
// backup, it will be lost.
|
||||
//
|
||||
// Note that this will clear all migrations.
|
||||
for (NSString *collection in collectionsToRestore) {
|
||||
[transaction removeAllObjectsInCollection:collection];
|
||||
}
|
||||
|
||||
NSUInteger count = 0;
|
||||
for (OWSBackupImportItem *item in self.databaseItems) {
|
||||
if (self.isComplete) {
|
||||
return;
|
||||
}
|
||||
if (item.recordName.length < 1) {
|
||||
DDLogError(@"%@ database snapshot was not downloaded.", self.logTag);
|
||||
// Attachment-related errors are recoverable and can be ignored.
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return completion(NO);
|
||||
}
|
||||
if (!item.uncompressedDataLength || item.uncompressedDataLength.unsignedIntValue < 1) {
|
||||
DDLogError(@"%@ database snapshot missing size.", self.logTag);
|
||||
// Attachment-related errors are recoverable and can be ignored.
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return completion(NO);
|
||||
}
|
||||
|
||||
for (NSString *collection in collectionsToRestore) {
|
||||
if ([collection isEqualToString:[OWSDatabaseMigration collection]]) {
|
||||
// It's okay if there are existing migrations; we'll clear those
|
||||
// before restoring.
|
||||
continue;
|
||||
count++;
|
||||
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_RESTORING_DATABASE",
|
||||
@"Indicates that the backup database is being restored.")
|
||||
progress:@(count / (CGFloat)self.databaseItems.count)];
|
||||
|
||||
NSData *_Nullable compressedData =
|
||||
[self.encryption decryptFileAsData:item.downloadFilePath encryptionKey:item.encryptionKey];
|
||||
if (!compressedData) {
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return completion(NO);
|
||||
}
|
||||
NSData *_Nullable uncompressedData =
|
||||
[self.encryption decompressData:compressedData
|
||||
uncompressedDataLength:item.uncompressedDataLength.unsignedIntValue];
|
||||
if (!uncompressedData) {
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return completion(NO);
|
||||
}
|
||||
OWSSignalServiceProtosBackupSnapshot *_Nullable entities =
|
||||
[OWSSignalServiceProtosBackupSnapshot parseFromData:uncompressedData];
|
||||
if (!entities || entities.entity.count < 1) {
|
||||
DDLogError(@"%@ missing entities.", self.logTag);
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return completion(NO);
|
||||
}
|
||||
for (OWSSignalServiceProtosBackupSnapshotBackupEntity *entity in entities.entity) {
|
||||
NSData *_Nullable entityData = entity.entityData;
|
||||
if (entityData.length < 1) {
|
||||
DDLogError(@"%@ missing entity data.", self.logTag);
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return completion(NO);
|
||||
}
|
||||
if ([dstTransaction numberOfKeysInCollection:collection] > 0) {
|
||||
DDLogError(@"%@ unexpected contents in database (%@).", self.logTag, collection);
|
||||
|
||||
__block TSYapDatabaseObject *object = nil;
|
||||
@try {
|
||||
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:entityData];
|
||||
object = [unarchiver decodeObjectForKey:@"root"];
|
||||
if (![object isKindOfClass:[object class]]) {
|
||||
DDLogError(@"%@ invalid decoded entity: %@.", self.logTag, [object class]);
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return completion(NO);
|
||||
}
|
||||
} @catch (NSException *exception) {
|
||||
DDLogError(@"%@ could not decode entity.", self.logTag);
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return completion(NO);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear existing database contents.
|
||||
//
|
||||
// This should be safe since we only ever import into an empty database.
|
||||
//
|
||||
// Note that if the app receives a message after registering and before restoring
|
||||
// backup, it will be lost.
|
||||
//
|
||||
// Note that this will clear all migrations.
|
||||
for (NSString *collection in collectionsToRestore) {
|
||||
[dstTransaction removeAllObjectsInCollection:collection];
|
||||
[object saveWithTransaction:transaction];
|
||||
copiedEntities++;
|
||||
NSString *collection = [object.class collection];
|
||||
NSUInteger restoredEntityCount = restoredEntityCounts[collection].unsignedIntValue;
|
||||
restoredEntityCounts[collection] = @(restoredEntityCount + 1);
|
||||
}
|
||||
|
||||
// Copy database entities.
|
||||
for (NSString *collection in collectionsToRestore) {
|
||||
[srcTransaction enumerateKeysAndObjectsInCollection:collection
|
||||
usingBlock:^(NSString *key, id object, BOOL *stop) {
|
||||
if (self.isComplete) {
|
||||
*stop = YES;
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
Class expectedType = collectionTypeMap[collection];
|
||||
OWSAssert(expectedType);
|
||||
if (![object isKindOfClass:expectedType]) {
|
||||
OWSProdLogAndFail(@"%@ unexpected class: %@ != %@",
|
||||
self.logTag,
|
||||
[object class],
|
||||
expectedType);
|
||||
return;
|
||||
}
|
||||
TSYapDatabaseObject *databaseObject = object;
|
||||
[databaseObject saveWithTransaction:dstTransaction];
|
||||
|
||||
NSUInteger count
|
||||
= restoredEntityCounts[collection].unsignedIntValue;
|
||||
restoredEntityCounts[collection] = @(count + 1);
|
||||
copiedEntities++;
|
||||
}];
|
||||
}
|
||||
}];
|
||||
}
|
||||
}];
|
||||
|
||||
if (aborted) {
|
||||
if (self.isComplete || aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (NSString *collection in collectionsToRestore) {
|
||||
Class expectedType = collectionTypeMap[collection];
|
||||
OWSAssert(expectedType);
|
||||
DDLogInfo(@"%@ copied %@ (%@): %@", self.logTag, expectedType, collection, restoredEntityCounts[collection]);
|
||||
for (NSString *collection in restoredEntityCounts) {
|
||||
DDLogInfo(@"%@ copied %@: %@", self.logTag, collection, restoredEntityCounts[collection]);
|
||||
}
|
||||
DDLogInfo(@"%@ copiedEntities: %llu", self.logTag, copiedEntities);
|
||||
|
||||
[self.backupStorage logFileSizes];
|
||||
|
||||
// Close the database.
|
||||
tempDBConnection = nil;
|
||||
self.backupStorage = nil;
|
||||
[self.primaryStorage logFileSizes];
|
||||
|
||||
completion(YES);
|
||||
}
|
||||
|
@ -530,45 +591,6 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
|
|||
});
|
||||
}
|
||||
|
||||
- (BOOL)restoreFileWithRecordName:(NSString *)recordName
|
||||
dstRelativePath:(NSString *)dstRelativePath
|
||||
dstDirPath:(NSString *)dstDirPath
|
||||
{
|
||||
OWSAssert(recordName);
|
||||
OWSAssert(dstDirPath.length > 0);
|
||||
|
||||
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
if (![recordName isKindOfClass:[NSString class]]) {
|
||||
DDLogError(@"%@ invalid record name in manifest: %@", self.logTag, [recordName class]);
|
||||
return NO;
|
||||
}
|
||||
if (![dstRelativePath isKindOfClass:[NSString class]]) {
|
||||
DDLogError(@"%@ invalid dstRelativePath in manifest: %@", self.logTag, [recordName class]);
|
||||
return NO;
|
||||
}
|
||||
NSString *dstFilePath = [dstDirPath stringByAppendingPathComponent:dstRelativePath];
|
||||
if ([NSFileManager.defaultManager fileExistsAtPath:dstFilePath]) {
|
||||
DDLogError(@"%@ skipping redundant file restore: %@.", self.logTag, dstFilePath);
|
||||
return YES;
|
||||
}
|
||||
NSString *downloadedFilePath = self.downloadedFileMap[recordName];
|
||||
if (![NSFileManager.defaultManager fileExistsAtPath:downloadedFilePath]) {
|
||||
DDLogError(@"%@ missing downloaded attachment file.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
NSError *error;
|
||||
BOOL success = [NSFileManager.defaultManager moveItemAtPath:downloadedFilePath toPath:dstFilePath error:&error];
|
||||
if (!success || error) {
|
||||
DDLogError(@"%@ could not restore attachment file.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
|
||||
DDLogError(@"%@ restored file: %@ (%@).", self.logTag, dstFilePath, dstRelativePath);
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -9,6 +9,7 @@ extern NSString *const kOWSBackup_ManifestKey_AttachmentFiles;
|
|||
extern NSString *const kOWSBackup_ManifestKey_RecordName;
|
||||
extern NSString *const kOWSBackup_ManifestKey_EncryptionKey;
|
||||
extern NSString *const kOWSBackup_ManifestKey_RelativeFilePath;
|
||||
extern NSString *const kOWSBackup_ManifestKey_DataSize;
|
||||
|
||||
typedef void (^OWSBackupJobBoolCompletion)(BOOL success);
|
||||
typedef void (^OWSBackupJobCompletion)(NSError *_Nullable error);
|
||||
|
@ -62,12 +63,6 @@ typedef void (^OWSBackupJobCompletion)(NSError *_Nullable error);
|
|||
- (void)failWithError:(NSError *)error;
|
||||
- (void)updateProgressWithDescription:(nullable NSString *)description progress:(nullable NSNumber *)progress;
|
||||
|
||||
#pragma mark - Database KeySpec
|
||||
|
||||
+ (nullable NSData *)loadDatabaseKeySpecWithKeychainKey:(NSString *)keychainKey;
|
||||
+ (BOOL)storeDatabaseKeySpec:(NSData *)data keychainKey:(NSString *)keychainKey;
|
||||
+ (BOOL)generateRandomDatabaseKeySpecWithKeychainKey:(NSString *)keychainKey;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -16,6 +16,7 @@ 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_DataSize = @"data_size";
|
||||
|
||||
NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService";
|
||||
|
||||
|
@ -146,51 +147,6 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService";
|
|||
});
|
||||
}
|
||||
|
||||
#pragma mark - Database KeySpec
|
||||
|
||||
+ (nullable NSData *)loadDatabaseKeySpecWithKeychainKey:(NSString *)keychainKey
|
||||
{
|
||||
OWSAssert(keychainKey.length > 0);
|
||||
|
||||
NSError *error;
|
||||
NSData *_Nullable value =
|
||||
[SAMKeychain passwordDataForService:kOWSBackup_KeychainService account:keychainKey error:&error];
|
||||
if (!value || error) {
|
||||
DDLogError(@"%@ could not load database keyspec: %@", self.logTag, error);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
+ (BOOL)storeDatabaseKeySpec:(NSData *)data keychainKey:(NSString *)keychainKey
|
||||
{
|
||||
OWSAssert(keychainKey.length > 0);
|
||||
OWSAssert(data.length > 0);
|
||||
|
||||
NSError *error;
|
||||
[SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
|
||||
BOOL success =
|
||||
[SAMKeychain setPasswordData:data forService:kOWSBackup_KeychainService account:keychainKey error:&error];
|
||||
if (!success || error) {
|
||||
OWSFail(@"%@ Could not store database keyspec: %@.", self.logTag, error);
|
||||
return NO;
|
||||
} else {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)generateRandomDatabaseKeySpecWithKeychainKey:(NSString *)keychainKey
|
||||
{
|
||||
OWSAssert(keychainKey.length > 0);
|
||||
|
||||
NSData *_Nullable databaseKeySpec = [Randomness generateRandomBytes:(int)kSQLCipherKeySpecLength];
|
||||
if (!databaseKeySpec) {
|
||||
OWSFail(@"%@ Could not generate database keyspec.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
|
||||
return [self storeDatabaseKeySpec:databaseKeySpec keychainKey:keychainKey];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
# See README.md in this dir for prerequisite setup.
|
||||
|
||||
|
||||
PROTOC=protoc \
|
||||
--plugin=/usr/local/bin/protoc-gen-objc \
|
||||
--proto_path="${HOME}/src/WhisperSystems/protobuf-objc/src/compiler/" \
|
||||
--proto_path="${HOME}/src/WhisperSystems/protobuf-objc/src/compiler/google/protobuf/" \
|
||||
--proto_path="${HOME}/code/workspace/ows/protobuf-objc/src/compiler/" \
|
||||
--proto_path="${HOME}/code/workspace/ows/protobuf-objc/src/compiler/google/protobuf/" \
|
||||
--proto_path='./'
|
||||
|
||||
all: signal_service_proto provisioning_protos fingerprint_protos
|
||||
|
|
|
@ -251,7 +251,7 @@ message BackupSnapshot
|
|||
ATTACHMENT = 4;
|
||||
}
|
||||
optional Type type = 1;
|
||||
optional bytes data = 2;
|
||||
optional bytes entityData = 2;
|
||||
}
|
||||
|
||||
repeated BackupEntity entity = 1;
|
||||
|
|
|
@ -2266,18 +2266,18 @@ NSString *NSStringFromOWSSignalServiceProtosBackupSnapshotBackupEntityType(OWSSi
|
|||
@end
|
||||
|
||||
#define BackupEntity_type @"type"
|
||||
#define BackupEntity_data @"data"
|
||||
#define BackupEntity_entityData @"entityData"
|
||||
@interface OWSSignalServiceProtosBackupSnapshotBackupEntity : PBGeneratedMessage<GeneratedMessageProtocol> {
|
||||
@private
|
||||
BOOL hasData_:1;
|
||||
BOOL hasEntityData_:1;
|
||||
BOOL hasType_:1;
|
||||
NSData* data;
|
||||
NSData* entityData;
|
||||
OWSSignalServiceProtosBackupSnapshotBackupEntityType type;
|
||||
}
|
||||
- (BOOL) hasType;
|
||||
- (BOOL) hasData;
|
||||
- (BOOL) hasEntityData;
|
||||
@property (readonly) OWSSignalServiceProtosBackupSnapshotBackupEntityType type;
|
||||
@property (readonly, strong) NSData* data;
|
||||
@property (readonly, strong) NSData* entityData;
|
||||
|
||||
+ (instancetype) defaultInstance;
|
||||
- (instancetype) defaultInstance;
|
||||
|
@ -2319,10 +2319,10 @@ NSString *NSStringFromOWSSignalServiceProtosBackupSnapshotBackupEntityType(OWSSi
|
|||
- (OWSSignalServiceProtosBackupSnapshotBackupEntityBuilder*) setType:(OWSSignalServiceProtosBackupSnapshotBackupEntityType) value;
|
||||
- (OWSSignalServiceProtosBackupSnapshotBackupEntityBuilder*) clearType;
|
||||
|
||||
- (BOOL) hasData;
|
||||
- (NSData*) data;
|
||||
- (OWSSignalServiceProtosBackupSnapshotBackupEntityBuilder*) setData:(NSData*) value;
|
||||
- (OWSSignalServiceProtosBackupSnapshotBackupEntityBuilder*) clearData;
|
||||
- (BOOL) hasEntityData;
|
||||
- (NSData*) entityData;
|
||||
- (OWSSignalServiceProtosBackupSnapshotBackupEntityBuilder*) setEntityData:(NSData*) value;
|
||||
- (OWSSignalServiceProtosBackupSnapshotBackupEntityBuilder*) clearEntityData;
|
||||
@end
|
||||
|
||||
@interface OWSSignalServiceProtosBackupSnapshotBuilder : PBGeneratedMessageBuilder {
|
||||
|
|
|
@ -9830,7 +9830,7 @@ static OWSSignalServiceProtosBackupSnapshot* defaultOWSSignalServiceProtosBackup
|
|||
|
||||
@interface OWSSignalServiceProtosBackupSnapshotBackupEntity ()
|
||||
@property OWSSignalServiceProtosBackupSnapshotBackupEntityType type;
|
||||
@property (strong) NSData* data;
|
||||
@property (strong) NSData* entityData;
|
||||
@end
|
||||
|
||||
@implementation OWSSignalServiceProtosBackupSnapshotBackupEntity
|
||||
|
@ -9842,17 +9842,17 @@ static OWSSignalServiceProtosBackupSnapshot* defaultOWSSignalServiceProtosBackup
|
|||
hasType_ = !!_value_;
|
||||
}
|
||||
@synthesize type;
|
||||
- (BOOL) hasData {
|
||||
return !!hasData_;
|
||||
- (BOOL) hasEntityData {
|
||||
return !!hasEntityData_;
|
||||
}
|
||||
- (void) setHasData:(BOOL) _value_ {
|
||||
hasData_ = !!_value_;
|
||||
- (void) setHasEntityData:(BOOL) _value_ {
|
||||
hasEntityData_ = !!_value_;
|
||||
}
|
||||
@synthesize data;
|
||||
@synthesize entityData;
|
||||
- (instancetype) init {
|
||||
if ((self = [super init])) {
|
||||
self.type = OWSSignalServiceProtosBackupSnapshotBackupEntityTypeUnknown;
|
||||
self.data = [NSData data];
|
||||
self.entityData = [NSData data];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
@ -9875,8 +9875,8 @@ static OWSSignalServiceProtosBackupSnapshotBackupEntity* defaultOWSSignalService
|
|||
if (self.hasType) {
|
||||
[output writeEnum:1 value:self.type];
|
||||
}
|
||||
if (self.hasData) {
|
||||
[output writeData:2 value:self.data];
|
||||
if (self.hasEntityData) {
|
||||
[output writeData:2 value:self.entityData];
|
||||
}
|
||||
[self.unknownFields writeToCodedOutputStream:output];
|
||||
}
|
||||
|
@ -9890,8 +9890,8 @@ static OWSSignalServiceProtosBackupSnapshotBackupEntity* defaultOWSSignalService
|
|||
if (self.hasType) {
|
||||
size_ += computeEnumSize(1, self.type);
|
||||
}
|
||||
if (self.hasData) {
|
||||
size_ += computeDataSize(2, self.data);
|
||||
if (self.hasEntityData) {
|
||||
size_ += computeDataSize(2, self.entityData);
|
||||
}
|
||||
size_ += self.unknownFields.serializedSize;
|
||||
memoizedSerializedSize = size_;
|
||||
|
@ -9931,8 +9931,8 @@ static OWSSignalServiceProtosBackupSnapshotBackupEntity* defaultOWSSignalService
|
|||
if (self.hasType) {
|
||||
[output appendFormat:@"%@%@: %@\n", indent, @"type", NSStringFromOWSSignalServiceProtosBackupSnapshotBackupEntityType(self.type)];
|
||||
}
|
||||
if (self.hasData) {
|
||||
[output appendFormat:@"%@%@: %@\n", indent, @"data", self.data];
|
||||
if (self.hasEntityData) {
|
||||
[output appendFormat:@"%@%@: %@\n", indent, @"entityData", self.entityData];
|
||||
}
|
||||
[self.unknownFields writeDescriptionTo:output withIndent:indent];
|
||||
}
|
||||
|
@ -9940,8 +9940,8 @@ static OWSSignalServiceProtosBackupSnapshotBackupEntity* defaultOWSSignalService
|
|||
if (self.hasType) {
|
||||
[dictionary setObject: @(self.type) forKey: @"type"];
|
||||
}
|
||||
if (self.hasData) {
|
||||
[dictionary setObject: self.data forKey: @"data"];
|
||||
if (self.hasEntityData) {
|
||||
[dictionary setObject: self.entityData forKey: @"entityData"];
|
||||
}
|
||||
[self.unknownFields storeInDictionary:dictionary];
|
||||
}
|
||||
|
@ -9956,8 +9956,8 @@ static OWSSignalServiceProtosBackupSnapshotBackupEntity* defaultOWSSignalService
|
|||
return
|
||||
self.hasType == otherMessage.hasType &&
|
||||
(!self.hasType || self.type == otherMessage.type) &&
|
||||
self.hasData == otherMessage.hasData &&
|
||||
(!self.hasData || [self.data isEqual:otherMessage.data]) &&
|
||||
self.hasEntityData == otherMessage.hasEntityData &&
|
||||
(!self.hasEntityData || [self.entityData isEqual:otherMessage.entityData]) &&
|
||||
(self.unknownFields == otherMessage.unknownFields || (self.unknownFields != nil && [self.unknownFields isEqual:otherMessage.unknownFields]));
|
||||
}
|
||||
- (NSUInteger) hash {
|
||||
|
@ -9965,8 +9965,8 @@ static OWSSignalServiceProtosBackupSnapshotBackupEntity* defaultOWSSignalService
|
|||
if (self.hasType) {
|
||||
hashCode = hashCode * 31 + self.type;
|
||||
}
|
||||
if (self.hasData) {
|
||||
hashCode = hashCode * 31 + [self.data hash];
|
||||
if (self.hasEntityData) {
|
||||
hashCode = hashCode * 31 + [self.entityData hash];
|
||||
}
|
||||
hashCode = hashCode * 31 + [self.unknownFields hash];
|
||||
return hashCode;
|
||||
|
@ -10043,8 +10043,8 @@ NSString *NSStringFromOWSSignalServiceProtosBackupSnapshotBackupEntityType(OWSSi
|
|||
if (other.hasType) {
|
||||
[self setType:other.type];
|
||||
}
|
||||
if (other.hasData) {
|
||||
[self setData:other.data];
|
||||
if (other.hasEntityData) {
|
||||
[self setEntityData:other.entityData];
|
||||
}
|
||||
[self mergeUnknownFields:other.unknownFields];
|
||||
return self;
|
||||
|
@ -10077,7 +10077,7 @@ NSString *NSStringFromOWSSignalServiceProtosBackupSnapshotBackupEntityType(OWSSi
|
|||
break;
|
||||
}
|
||||
case 18: {
|
||||
[self setData:[input readData]];
|
||||
[self setEntityData:[input readData]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -10099,20 +10099,20 @@ NSString *NSStringFromOWSSignalServiceProtosBackupSnapshotBackupEntityType(OWSSi
|
|||
resultBackupEntity.type = OWSSignalServiceProtosBackupSnapshotBackupEntityTypeUnknown;
|
||||
return self;
|
||||
}
|
||||
- (BOOL) hasData {
|
||||
return resultBackupEntity.hasData;
|
||||
- (BOOL) hasEntityData {
|
||||
return resultBackupEntity.hasEntityData;
|
||||
}
|
||||
- (NSData*) data {
|
||||
return resultBackupEntity.data;
|
||||
- (NSData*) entityData {
|
||||
return resultBackupEntity.entityData;
|
||||
}
|
||||
- (OWSSignalServiceProtosBackupSnapshotBackupEntityBuilder*) setData:(NSData*) value {
|
||||
resultBackupEntity.hasData = YES;
|
||||
resultBackupEntity.data = value;
|
||||
- (OWSSignalServiceProtosBackupSnapshotBackupEntityBuilder*) setEntityData:(NSData*) value {
|
||||
resultBackupEntity.hasEntityData = YES;
|
||||
resultBackupEntity.entityData = value;
|
||||
return self;
|
||||
}
|
||||
- (OWSSignalServiceProtosBackupSnapshotBackupEntityBuilder*) clearData {
|
||||
resultBackupEntity.hasData = NO;
|
||||
resultBackupEntity.data = [NSData data];
|
||||
- (OWSSignalServiceProtosBackupSnapshotBackupEntityBuilder*) clearEntityData {
|
||||
resultBackupEntity.hasEntityData = NO;
|
||||
resultBackupEntity.entityData = [NSData data];
|
||||
return self;
|
||||
}
|
||||
@end
|
||||
|
|
|
@ -46,14 +46,6 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void);
|
|||
|
||||
#pragma mark -
|
||||
|
||||
//@interface OWSDatabaseConnection : YapDatabaseConnection
|
||||
//
|
||||
//@property (atomic, weak) id<OWSDatabaseConnectionDelegate> delegate;
|
||||
//
|
||||
//@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSDatabaseConnection
|
||||
|
||||
- (id)initWithDatabase:(YapDatabase *)database delegate:(id<OWSDatabaseConnectionDelegate>)delegate
|
||||
|
|
Loading…
Reference in a new issue