From 8769fb07cf4531e960915b0843ec2280eb1b0888 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 4 Jan 2018 15:16:24 -0500 Subject: [PATCH] Write backup to encrypted zip. --- Podfile | 1 + Podfile.lock | 5 +- Signal.xcodeproj/project.pbxproj | 16 +- .../src/ViewControllers/DebugUI/DebugUIMisc.m | 5 + Signal/src/util/OWSBackup.h | 13 + Signal/src/util/OWSBackup.m | 230 ++++++++++++++++++ 6 files changed, 263 insertions(+), 7 deletions(-) create mode 100644 Signal/src/util/OWSBackup.h create mode 100644 Signal/src/util/OWSBackup.m diff --git a/Podfile b/Podfile index b9befc447..06e46e074 100644 --- a/Podfile +++ b/Podfile @@ -31,6 +31,7 @@ end target 'Signal' do shared_pods pod 'ATAppUpdater', :inhibit_warnings => true + pod 'SSZipArchive', :inhibit_warnings => true target 'SignalTests' do inherit! :search_paths diff --git a/Podfile.lock b/Podfile.lock index e43ee2103..f5f399562 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -59,6 +59,7 @@ PODS: - SQLCipher/common (3.4.1) - SQLCipher/standard (3.4.1): - SQLCipher/common + - SSZipArchive (2.1.1) - TwistedOakCollapsingFutures (1.0.0): - UnionFind (~> 1.0) - UnionFind (1.0.1) @@ -138,6 +139,7 @@ DEPENDENCIES: - Reachability - SignalServiceKit (from `.`) - SocketRocket (from `https://github.com/facebook/SocketRocket.git`) + - SSZipArchive - YapDatabase/SQLCipher - YYImage @@ -200,11 +202,12 @@ SPEC CHECKSUMS: SignalServiceKit: 8f9038e584080bee8c367268067e6e0ec0feefcf SocketRocket: dbb1554b8fc288ef8ef370d6285aeca7361be31e SQLCipher: 43d12c0eb9c57fb438749618fc3ce0065509a559 + SSZipArchive: 14401ade5f8e82aba1ff03e9f88e9de60937ae60 TwistedOakCollapsingFutures: f359b90f203e9ab13dfb92c9ff41842a7fe1cd0c UnionFind: c33be5adb12983981d6e827ea94fc7f9e370f52d YapDatabase: 299a32de9d350d37a9ac5b0532609d87d5d2a5de YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54 -PODFILE CHECKSUM: bbc38aeceb39482a3cc46f0de44c6aef671e3e9f +PODFILE CHECKSUM: fe273b089523b52e20652cebcf4854a80934f46c COCOAPODS: 1.3.1 diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 09ccad5e8..9133a7533 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -119,6 +119,7 @@ 347850721FDAEB17007B8332 /* OWSUserProfile.h in Headers */ = {isa = PBXBuildFile; fileRef = 347850701FDAEB16007B8332 /* OWSUserProfile.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3497DBEC1ECE257500DB2605 /* OWSCountryMetadata.m in Sources */ = {isa = PBXBuildFile; fileRef = 3497DBEB1ECE257500DB2605 /* OWSCountryMetadata.m */; }; 3497DBEF1ECE2E4700DB2605 /* DomainFrontingCountryViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3497DBEE1ECE2E4700DB2605 /* DomainFrontingCountryViewController.m */; }; + 34A910601FFEB114000C4745 /* OWSBackup.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A9105F1FFEB114000C4745 /* OWSBackup.m */; }; 34B0796D1FCF46B100E248C2 /* MainAppContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B0796B1FCF46B000E248C2 /* MainAppContext.m */; }; 34B3F8711E8DF1700035BE1A /* AboutTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8351E8DF1700035BE1A /* AboutTableViewController.m */; }; 34B3F8721E8DF1700035BE1A /* AdvancedSettingsTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8371E8DF1700035BE1A /* AdvancedSettingsTableViewController.m */; }; @@ -603,6 +604,8 @@ 3497DBEB1ECE257500DB2605 /* OWSCountryMetadata.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSCountryMetadata.m; sourceTree = ""; }; 3497DBED1ECE2E4700DB2605 /* DomainFrontingCountryViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DomainFrontingCountryViewController.h; sourceTree = ""; }; 3497DBEE1ECE2E4700DB2605 /* DomainFrontingCountryViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DomainFrontingCountryViewController.m; sourceTree = ""; }; + 34A9105E1FFEB113000C4745 /* OWSBackup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackup.h; sourceTree = ""; }; + 34A9105F1FFEB114000C4745 /* OWSBackup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackup.m; sourceTree = ""; }; 34B0796B1FCF46B000E248C2 /* MainAppContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MainAppContext.m; sourceTree = ""; }; 34B0796C1FCF46B000E248C2 /* MainAppContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MainAppContext.h; sourceTree = ""; }; 34B0796E1FD07B1E00E248C2 /* SignalShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SignalShareExtension.entitlements; sourceTree = ""; }; @@ -1703,8 +1706,8 @@ 76EB04C818170B33006006FC /* util */ = { isa = PBXGroup; children = ( - B6DA6B061B8A2F9A00CA6F98 /* AppStoreRating.m */, B6DA6B051B8A2F9A00CA6F98 /* AppStoreRating.h */, + B6DA6B061B8A2F9A00CA6F98 /* AppStoreRating.m */, 34CCAF361F0C0599004084F4 /* AppUpdateNag.h */, 34CCAF371F0C0599004084F4 /* AppUpdateNag.m */, B90418E4183E9DD40038554A /* DateUtil.h */, @@ -1717,6 +1720,8 @@ 76EB04EC18170B33006006FC /* NumberUtil.h */, 76EB04ED18170B33006006FC /* NumberUtil.m */, 34D99C911F2937CC00D284D6 /* OWSAnalytics.swift */, + 34A9105E1FFEB113000C4745 /* OWSBackup.h */, + 34A9105F1FFEB114000C4745 /* OWSBackup.m */, 4579431C1E7C8CE9008ED0C0 /* Pastelog.h */, 4579431D1E7C8CE9008ED0C0 /* Pastelog.m */, 450DF2041E0D74AC003D14BE /* Platform.swift */, @@ -2527,6 +2532,7 @@ "${BUILT_PRODUCTS_DIR}/Reachability/Reachability.framework", "${BUILT_PRODUCTS_DIR}/SAMKeychain/SAMKeychain.framework", "${BUILT_PRODUCTS_DIR}/SQLCipher/SQLCipher.framework", + "${BUILT_PRODUCTS_DIR}/SSZipArchive/SSZipArchive.framework", "${BUILT_PRODUCTS_DIR}/SignalServiceKit/SignalServiceKit.framework", "${BUILT_PRODUCTS_DIR}/SocketRocket/SocketRocket.framework", "${BUILT_PRODUCTS_DIR}/TwistedOakCollapsingFutures/TwistedOakCollapsingFutures.framework", @@ -2552,6 +2558,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SAMKeychain.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SQLCipher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SSZipArchive.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SignalServiceKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SocketRocket.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/TwistedOakCollapsingFutures.framework", @@ -2945,6 +2952,7 @@ 34E8BF381EE9E2FD00F5F4CA /* FingerprintViewScanController.m in Sources */, 346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */, 45E5A6991F61E6DE001E4A8A /* MarqueeLabel.swift in Sources */, + 34A910601FFEB114000C4745 /* OWSBackup.m in Sources */, 34D1F0B01F867BFC0066283D /* OWSSystemMessageCell.m in Sources */, 45A663C51F92EC760027B59E /* GroupTableViewCell.swift in Sources */, 34D1F0861F8678AA0066283D /* ConversationViewController.m in Sources */, @@ -3106,11 +3114,7 @@ "DEBUG=1", "$(inherited)", ); - "GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = ( - "DEBUG=1", - "$(inherited)", - "SSK_BUILDING_FOR_TESTS=1", - ); + "GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = "DEBUG=1 $(inherited) SSK_BUILDING_FOR_TESTS=1"; GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m index 9a46423df..59f23628f 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m @@ -3,6 +3,7 @@ // #import "DebugUIMisc.h" +#import "OWSBackup.h" #import "OWSCountryMetadata.h" #import "OWSTableViewController.h" #import "RegistrationViewController.h" @@ -93,6 +94,10 @@ NS_ASSUME_NONNULL_BEGIN [DebugUIMisc sendUnencryptedDatabase:thread]; }]]; } + [items addObject:[OWSTableItem itemWithTitle:@"Export Database" + actionBlock:^{ + [OWSBackup exportDatabase]; + }]]; return [OWSTableSection sectionWithTitle:self.name items:items]; } diff --git a/Signal/src/util/OWSBackup.h b/Signal/src/util/OWSBackup.h new file mode 100644 index 000000000..a940e2238 --- /dev/null +++ b/Signal/src/util/OWSBackup.h @@ -0,0 +1,13 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSBackup : NSObject + ++ (void)exportDatabase; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/util/OWSBackup.m b/Signal/src/util/OWSBackup.m new file mode 100644 index 000000000..ab40aad9f --- /dev/null +++ b/Signal/src/util/OWSBackup.m @@ -0,0 +1,230 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSBackup.h" +#import "NSUserDefaults+OWS.h" +#import "zlib.h" +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSStorage (OWSBackup) + +- (NSData *)databasePassword; + +@end + +#pragma mark - + +@interface OWSBackup () + +@property (nonatomic) NSString *rootDirPath; + +@end + +#pragma mark - + +@implementation OWSBackup + +- (void)dealloc +{ + OWSAssert(self.rootDirPath.length > 0); + + DDLogInfo(@"%@ Cleaning up: %@", self.logTag, self.rootDirPath); + [OWSFileSystem deleteFile:self.rootDirPath]; +} + ++ (void)exportDatabase +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [[OWSBackup new] exportDatabase]; + }); +} + +- (void)exportDatabase +{ + NSString *temporaryDirectory = NSTemporaryDirectory(); + NSString *backupName = [NSUUID UUID].UUIDString; + NSString *rootDirPath = [temporaryDirectory stringByAppendingPathComponent:backupName]; + self.rootDirPath = rootDirPath; + NSString *backupDirPath = [rootDirPath stringByAppendingPathComponent:@"Contents"]; + NSString *backupZipPath = [rootDirPath stringByAppendingPathComponent:[backupName stringByAppendingString:@".zip"]]; + DDLogInfo(@"%@ rootDirPath: %@", self.logTag, rootDirPath); + DDLogInfo(@"%@ backupDirPath: %@", self.logTag, backupDirPath); + DDLogInfo(@"%@ backupZipPath: %@", self.logTag, backupZipPath); + + [OWSFileSystem ensureDirectoryExists:rootDirPath]; + [OWSFileSystem protectFolderAtPath:rootDirPath]; + [OWSFileSystem ensureDirectoryExists:backupDirPath]; + + NSData *databasePassword = [TSStorageManager sharedManager].databasePassword; + + if (![self writeData:databasePassword fileName:@"databasePassword" backupDirPath:backupDirPath]) { + return; + } + if (![self writeUserDefaults:NSUserDefaults.standardUserDefaults + fileName:@"standardUserDefaults" + backupDirPath:backupDirPath]) { + return; + } + if (![self writeUserDefaults:NSUserDefaults.appUserDefaults + fileName:@"appUserDefaults" + backupDirPath:backupDirPath]) { + return; + } + if (![self copyDirectory:OWSFileSystem.appDocumentDirectoryPath + dstDirName:@"appDocumentDirectoryPath" + backupDirPath:backupDirPath]) { + return; + } + if (![self copyDirectory:OWSFileSystem.appSharedDataDirectoryPath + dstDirName:@"appSharedDataDirectoryPath" + backupDirPath:backupDirPath]) { + return; + } + if (![self zipDirectory:backupDirPath dstFilePath:backupZipPath]) { + return; + } + + [OWSFileSystem protectFolderAtPath:backupZipPath]; +} + +- (BOOL)writeData:(NSData *)data fileName:(NSString *)fileName backupDirPath:(NSString *)backupDirPath +{ + OWSAssert(data); + OWSAssert(fileName.length > 0); + OWSAssert(backupDirPath.length > 0); + + NSString *filePath = [backupDirPath stringByAppendingPathComponent:fileName]; + NSError *error; + BOOL success = [data writeToFile:filePath options:NSDataWritingAtomic error:&error]; + if (!success || error) { + OWSFail(@"%@ failed to write user defaults: %@", self.logTag, error); + return NO; + } + return YES; +} + +- (BOOL)copyDirectory:(NSString *)srcDirPath dstDirName:(NSString *)dstDirName backupDirPath:(NSString *)backupDirPath +{ + OWSAssert(srcDirPath.length > 0); + OWSAssert(dstDirName.length > 0); + OWSAssert(backupDirPath.length > 0); + + NSString *dstDirPath = [backupDirPath stringByAppendingPathComponent:dstDirName]; + + NSError *error = nil; + BOOL success = [[NSFileManager defaultManager] copyItemAtPath:srcDirPath toPath:dstDirPath error:&error]; + if (!success || error) { + OWSFail(@"%@ failed to copy directory: %@, %@", self.logTag, dstDirName, error); + return NO; + } + return YES; +} + +- (BOOL)writeUserDefaults:(NSUserDefaults *)userDefaults + fileName:(NSString *)fileName + backupDirPath:(NSString *)backupDirPath +{ + OWSAssert(userDefaults); + OWSAssert(fileName.length > 0); + OWSAssert(backupDirPath.length > 0); + + NSDictionary *dictionary = userDefaults.dictionaryRepresentation; + if (!dictionary) { + OWSFail(@"%@ failed to extract user defaults", self.logTag); + return NO; + } + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:dictionary]; + if (!data) { + OWSFail(@"%@ failed to archive user defaults", self.logTag); + return NO; + } + + return [self writeData:data fileName:fileName backupDirPath:backupDirPath]; +} + +- (BOOL)zipDirectory:(NSString *)srcDirPath dstFilePath:(NSString *)dstFilePath +{ + OWSAssert(srcDirPath.length > 0); + OWSAssert(dstFilePath.length > 0); + + // TODO: + NSString *password = [NSUUID UUID].UUIDString; + + BOOL success = [SSZipArchive createZipFileAtPath:dstFilePath + withContentsOfDirectory:srcDirPath + keepParentDirectory:NO + compressionLevel:Z_DEFAULT_COMPRESSION + password:password + AES:YES + progressHandler:^(NSUInteger entryNumber, NSUInteger total) { + DDLogVerbose(@"%@ Zip progress: %zd / %zd = %f", + self.logTag, + entryNumber, + total, + entryNumber / (CGFloat)total); + // TODO: + }]; + + if (!success) { + OWSFail(@"%@ failed to write zip backup", self.logTag); + return NO; + } + + NSError *error; + NSNumber *fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:dstFilePath error:&error][NSFileSize]; + if (error) { + OWSFail(@"%@ failed to get zip file size: %@", self.logTag, error); + return NO; + } + DDLogVerbose(@"%@ Zip file size: %@", self.logTag, fileSize); + + return YES; +} + +//- (BOOL)zipDirectory:(NSString *)srcDirPath +// rootSrcDirPath:(NSString *)rootSrcDirPath +// zipFile:(OZZipFile *)zipFile +//{ +// OWSAssert(srcDirPath.length > 0); +// OWSAssert(rootSrcDirPath.length > 0); +// OWSAssert(zipFile); +// +// NSFileManager *fileManager = [NSFileManager defaultManager] ; +// NSError *error; +// NSArray *filenames =[fileManager contentsOfDirectoryAtPath:srcDirPath error:&error]; +// if (error) { +// OWSFail(@"%@ failed to get directory contents: %@", self.logTag, error); +// return NO; +// } +// for (NSString *fileName in filenames) { +// +// } +// +// // OZZipWriteStream *stream= [zipFile writeFileInZipWithName:@"abc.txt" +// // compressionLevel:OZZipCompressionLevelBest]; +// // +// // [stream writeData:abcData]; +// // [stream finishedWriting];} +// // +// // NSData *fileData= // Your file data +// // uint32_t crc= [fileData crc32]; +// // +// // OZZipWriteStream *stream= [zipFile writeFileInZipWithName:@"abc.txt" +// // compressionLevel:OZZipCompressionLevelBest +// // password:@"password" +// // crc32:crc]; +// // +// //[stream writeData:fileData]; +// [stream finishedWriting]; +// +// return YES; +//} + +@end + +NS_ASSUME_NONNULL_END