From 8769fb07cf4531e960915b0843ec2280eb1b0888 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 4 Jan 2018 15:16:24 -0500 Subject: [PATCH 01/15] 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 b9befc447e..06e46e074f 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 e43ee21030..f5f3995620 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 09ccad5e89..9133a7533e 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 9a46423df9..59f23628fe 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 0000000000..a940e22382 --- /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 0000000000..ab40aad9fc --- /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 From 2789c0f12cc69c2a71565fee10d36dceeee0958c Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 4 Jan 2018 17:16:26 -0500 Subject: [PATCH 02/15] Write backup to encrypted zip. --- Signal/src/util/OWSBackup.m | 138 ++++++++++++++++++++++++------------ 1 file changed, 92 insertions(+), 46 deletions(-) diff --git a/Signal/src/util/OWSBackup.m b/Signal/src/util/OWSBackup.m index ab40aad9fc..055027bedc 100644 --- a/Signal/src/util/OWSBackup.m +++ b/Signal/src/util/OWSBackup.m @@ -4,8 +4,10 @@ #import "OWSBackup.h" #import "NSUserDefaults+OWS.h" +#import "Signal-Swift.h" #import "zlib.h" #import +#import #import #import @@ -21,7 +23,9 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSBackup () +@property (nonatomic) NSString *password; @property (nonatomic) NSString *rootDirPath; +@property (atomic) BOOL cancelled; @end @@ -34,16 +38,99 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(self.rootDirPath.length > 0); DDLogInfo(@"%@ Cleaning up: %@", self.logTag, self.rootDirPath); - [OWSFileSystem deleteFile:self.rootDirPath]; + [OWSFileSystem deleteFileIfExists:self.rootDirPath]; } + (void)exportDatabase { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [[OWSBackup new] exportDatabase]; + dispatch_async(dispatch_get_main_queue(), ^{ + [[OWSBackup new] showBackupUI]; }); } +- (void)showBackupUI +{ + // TODO: Should the user pick a password? + NSString *password = [NSUUID UUID].UUIDString; + self.password = password; + DDLogVerbose(@"%@ backup export complete; password: %@", self.logTag, password); + + [self showExportProgressUI:^(UIAlertController *exportProgressAlert) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self exportDatabase]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [exportProgressAlert + dismissViewControllerAnimated:YES + completion:^{ + [self showExportCompleteUI:^(UIAlertController *exportCompleteAlert){ + }]; + }]; + }); + }); + }]; +} + +- (void)showExportProgressUI:(void (^_Nonnull)(UIAlertController *))completion +{ + OWSAssert(completion); + OWSAssert(self.password.length > 0); + + NSString *title = NSLocalizedString( + @"BACKUP_EXPORT_IN_PROGRESS_ALERT_TITLE", @"Title for the 'backup export in progress' alert."); + NSString *message = [NSString + stringWithFormat: + NSLocalizedString(@"BACKUP_EXPORT_IN_PROGRESS_MESSAGE_ALERT_FORMAT", + @"Format for message for the 'backup export in progress' alert. Embeds: {{the backup password}}"), + self.password]; + UIAlertController *alert = + [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:[CommonStrings cancelButton] + style:UIAlertActionStyleCancel + handler:^(UIAlertAction *_Nonnull action) { + self.cancelled = YES; + }]; + [alert addAction:cancelAction]; + + UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController]; + [fromViewController presentViewController:alert + animated:YES + completion:^(void) { + completion(alert); + }]; +} + +- (void)showExportCompleteUI:(void (^_Nonnull)(UIAlertController *))completion +{ + OWSAssert(completion); + OWSAssert(self.password.length > 0); + + NSString *title + = NSLocalizedString(@"BACKUP_EXPORT_COMPLETE_ALERT_TITLE", @"Title for the 'backup export complete' alert."); + NSString *message = [NSString + stringWithFormat: + NSLocalizedString(@"BACKUP_EXPORT_COMPLETE_ALERT_MESSAGE_FORMAT", + @"Format for message for the 'backup export complete' alert. Embeds: {{the backup password}}"), + self.password]; + UIAlertController *alert = + [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:[CommonStrings dismissButton] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *_Nonnull action) { + self.cancelled = YES; + }]; + [alert addAction:dismissAction]; + + UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController]; + [fromViewController presentViewController:alert + animated:YES + completion:^(void) { + completion(alert); + }]; +} + - (void)exportDatabase { NSString *temporaryDirectory = NSTemporaryDirectory(); @@ -151,15 +238,13 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssert(srcDirPath.length > 0); OWSAssert(dstFilePath.length > 0); - - // TODO: - NSString *password = [NSUUID UUID].UUIDString; + OWSAssert(self.password.length > 0); BOOL success = [SSZipArchive createZipFileAtPath:dstFilePath withContentsOfDirectory:srcDirPath keepParentDirectory:NO compressionLevel:Z_DEFAULT_COMPRESSION - password:password + password:self.password AES:YES progressHandler:^(NSUInteger entryNumber, NSUInteger total) { DDLogVerbose(@"%@ Zip progress: %zd / %zd = %f", @@ -186,45 +271,6 @@ NS_ASSUME_NONNULL_BEGIN 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 From ea945558c8da75215981f42c2d5a7d7c6cf22b76 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 4 Jan 2018 17:47:33 -0500 Subject: [PATCH 03/15] Show share UI for backups. --- .../src/ViewControllers/DebugUI/DebugUIMisc.m | 4 +- Signal/src/util/OWSBackup.h | 2 +- Signal/src/util/OWSBackup.m | 104 +++++++++++------- .../translations/en.lproj/Localizable.strings | 12 ++ 4 files changed, 79 insertions(+), 43 deletions(-) diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m index 59f23628fe..b915111c3e 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m @@ -94,9 +94,9 @@ NS_ASSUME_NONNULL_BEGIN [DebugUIMisc sendUnencryptedDatabase:thread]; }]]; } - [items addObject:[OWSTableItem itemWithTitle:@"Export Database" + [items addObject:[OWSTableItem itemWithTitle:@"Export Backup" actionBlock:^{ - [OWSBackup exportDatabase]; + [OWSBackup exportBackup]; }]]; return [OWSTableSection sectionWithTitle:self.name items:items]; diff --git a/Signal/src/util/OWSBackup.h b/Signal/src/util/OWSBackup.h index a940e22382..3c28653e0f 100644 --- a/Signal/src/util/OWSBackup.h +++ b/Signal/src/util/OWSBackup.h @@ -6,7 +6,7 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSBackup : NSObject -+ (void)exportDatabase; ++ (void)exportBackup; @end diff --git a/Signal/src/util/OWSBackup.m b/Signal/src/util/OWSBackup.m index 055027bedc..52b1b3f8c0 100644 --- a/Signal/src/util/OWSBackup.m +++ b/Signal/src/util/OWSBackup.m @@ -7,6 +7,7 @@ #import "Signal-Swift.h" #import "zlib.h" #import +#import #import #import #import @@ -24,7 +25,8 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSBackup () @property (nonatomic) NSString *password; -@property (nonatomic) NSString *rootDirPath; +@property (nonatomic) NSString *backupDirPath; +@property (nonatomic) NSString *backupZipPath; @property (atomic) BOOL cancelled; @end @@ -35,21 +37,23 @@ NS_ASSUME_NONNULL_BEGIN - (void)dealloc { - OWSAssert(self.rootDirPath.length > 0); + OWSAssert(self.backupDirPath.length > 0); - DDLogInfo(@"%@ Cleaning up: %@", self.logTag, self.rootDirPath); - [OWSFileSystem deleteFileIfExists:self.rootDirPath]; + DDLogInfo(@"%@ Cleaning up: %@", self.logTag, self.backupDirPath); + [OWSFileSystem deleteFileIfExists:self.backupDirPath]; } -+ (void)exportDatabase ++ (void)exportBackup { dispatch_async(dispatch_get_main_queue(), ^{ - [[OWSBackup new] showBackupUI]; + [[OWSBackup new] exportBackup]; }); } -- (void)showBackupUI +- (void)exportBackup { + OWSAssertIsOnMainThread(); + // TODO: Should the user pick a password? NSString *password = [NSUUID UUID].UUIDString; self.password = password; @@ -57,15 +61,15 @@ NS_ASSUME_NONNULL_BEGIN [self showExportProgressUI:^(UIAlertController *exportProgressAlert) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [self exportDatabase]; + [self exportToFilesAndZip]; dispatch_async(dispatch_get_main_queue(), ^{ - [exportProgressAlert - dismissViewControllerAnimated:YES - completion:^{ - [self showExportCompleteUI:^(UIAlertController *exportCompleteAlert){ - }]; - }]; + [exportProgressAlert dismissViewControllerAnimated:YES + completion:^{ + [self showExportCompleteUI:^{ + [self showShareUI]; + }]; + }]; }); }); }]; @@ -73,18 +77,13 @@ NS_ASSUME_NONNULL_BEGIN - (void)showExportProgressUI:(void (^_Nonnull)(UIAlertController *))completion { + OWSAssertIsOnMainThread(); OWSAssert(completion); - OWSAssert(self.password.length > 0); NSString *title = NSLocalizedString( @"BACKUP_EXPORT_IN_PROGRESS_ALERT_TITLE", @"Title for the 'backup export in progress' alert."); - NSString *message = [NSString - stringWithFormat: - NSLocalizedString(@"BACKUP_EXPORT_IN_PROGRESS_MESSAGE_ALERT_FORMAT", - @"Format for message for the 'backup export in progress' alert. Embeds: {{the backup password}}"), - self.password]; UIAlertController *alert = - [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; + [UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:[CommonStrings cancelButton] style:UIAlertActionStyleCancel @@ -93,16 +92,23 @@ NS_ASSUME_NONNULL_BEGIN }]; [alert addAction:cancelAction]; + __weak UIAlertController *weakAlert = alert; UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController]; [fromViewController presentViewController:alert animated:YES completion:^(void) { - completion(alert); + UIAlertController *strongAlert = weakAlert; + if (!strongAlert) { + OWSFail(@"%@ missing alert.", self.logTag); + return; + } + completion(strongAlert); }]; } -- (void)showExportCompleteUI:(void (^_Nonnull)(UIAlertController *))completion +- (void)showExportCompleteUI:(void (^_Nonnull)(void))completion { + OWSAssertIsOnMainThread(); OWSAssert(completion); OWSAssert(self.password.length > 0); @@ -111,34 +117,50 @@ NS_ASSUME_NONNULL_BEGIN NSString *message = [NSString stringWithFormat: NSLocalizedString(@"BACKUP_EXPORT_COMPLETE_ALERT_MESSAGE_FORMAT", - @"Format for message for the 'backup export complete' alert. Embeds: {{the backup password}}"), + @"Format for message for the 'backup export complete' alert. Embeds: {{the backup password}}."), self.password]; UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:[CommonStrings dismissButton] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *_Nonnull action) { - self.cancelled = YES; - }]; - [alert addAction:dismissAction]; + UIAlertAction *okAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", comment + : nil) + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *_Nonnull action) { + completion(); + }]; + [alert addAction:okAction]; UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController]; - [fromViewController presentViewController:alert - animated:YES - completion:^(void) { - completion(alert); - }]; + [fromViewController presentViewController:alert animated:YES completion:nil]; } -- (void)exportDatabase +- (void)showShareUI +{ + OWSAssertIsOnMainThread(); + OWSAssert(self.backupZipPath.length > 0); + + [AttachmentSharing showShareUIForURL:[NSURL fileURLWithPath:self.backupZipPath]]; +} + +- (void)exportToFilesAndZip { NSString *temporaryDirectory = NSTemporaryDirectory(); - NSString *backupName = [NSUUID UUID].UUIDString; - NSString *rootDirPath = [temporaryDirectory stringByAppendingPathComponent:backupName]; - self.rootDirPath = rootDirPath; + NSString *rootDirPath = [temporaryDirectory stringByAppendingPathComponent:[NSUUID UUID].UUIDString]; NSString *backupDirPath = [rootDirPath stringByAppendingPathComponent:@"Contents"]; - NSString *backupZipPath = [rootDirPath stringByAppendingPathComponent:[backupName stringByAppendingString:@".zip"]]; + + NSDateFormatter *dateFormatter = [NSDateFormatter new]; + [dateFormatter setLocale:[NSLocale currentLocale]]; + [dateFormatter setDateFormat:@"yyyy.MM.dd hh.mm.ss"]; + NSString *backupDateTime = [dateFormatter stringFromDate:[NSDate new]]; + NSString *backupName = + [NSString stringWithFormat:NSLocalizedString(@"BACKUP_FILENAME_FORMAT", + @"Format for backup filenames. Embeds: {{the date and time of the backup}}. " + @"Should not include characters like slash (/ or \\) or colon (:)."), + backupDateTime]; + NSString *backupZipPath = + [rootDirPath stringByAppendingPathComponent:[backupName stringByAppendingString:@".signalbackup"]]; + self.backupDirPath = backupDirPath; + self.backupZipPath = backupZipPath; DDLogInfo(@"%@ rootDirPath: %@", self.logTag, rootDirPath); DDLogInfo(@"%@ backupDirPath: %@", self.logTag, backupDirPath); DDLogInfo(@"%@ backupZipPath: %@", self.logTag, backupZipPath); @@ -177,6 +199,8 @@ NS_ASSUME_NONNULL_BEGIN } [OWSFileSystem protectFolderAtPath:backupZipPath]; + + [OWSFileSystem deleteFileIfExists:self.backupDirPath]; } - (BOOL)writeData:(NSData *)data fileName:(NSString *)fileName backupDirPath:(NSString *)backupDirPath diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index e93e381b15..86faee4f01 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -145,6 +145,18 @@ /* action sheet button title to enable built in speaker during a call */ "AUDIO_ROUTE_BUILT_IN_SPEAKER" = "Speaker"; +/* Format for message for the 'backup export complete' alert. Embeds: {{the backup password}} */ +"BACKUP_EXPORT_COMPLETE_ALERT_MESSAGE_FORMAT" = "Your backup password is: %@. Make sure to keep a copy of this password or you won't be able to restore from this backup."; + +/* Title for the 'backup export complete' alert. */ +"BACKUP_EXPORT_COMPLETE_ALERT_TITLE" = "Backup Complete"; + +/* Title for the 'backup export in progress' alert. */ +"BACKUP_EXPORT_IN_PROGRESS_ALERT_TITLE" = "Exporting Backup..."; + +/* Format for backup filenames. Embeds: {{the date and time of the backup}} */ +"BACKUP_FILENAME_FORMAT" = "Signal Backup %@"; + /* An explanation of the consequences of blocking another user. */ "BLOCK_BEHAVIOR_EXPLANATION" = "Blocked users will not be able to call you or send you messages."; From 2011dae8b6040dfe07fc385af73be2295b6809e3 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 4 Jan 2018 17:49:57 -0500 Subject: [PATCH 04/15] Show share UI for backups. --- Signal/src/util/OWSBackup.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Signal/src/util/OWSBackup.m b/Signal/src/util/OWSBackup.m index 52b1b3f8c0..5ac9821642 100644 --- a/Signal/src/util/OWSBackup.m +++ b/Signal/src/util/OWSBackup.m @@ -55,6 +55,8 @@ NS_ASSUME_NONNULL_BEGIN OWSAssertIsOnMainThread(); // TODO: Should the user pick a password? + // If not, should probably generate something more user-friendly, + // e.g. case-insensitive set of hexadecimal? NSString *password = [NSUUID UUID].UUIDString; self.password = password; DDLogVerbose(@"%@ backup export complete; password: %@", self.logTag, password); @@ -112,6 +114,8 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(completion); OWSAssert(self.password.length > 0); + // TODO: We probably want to offer an option that lets users copy + // the password to the pasteboard. NSString *title = NSLocalizedString(@"BACKUP_EXPORT_COMPLETE_ALERT_TITLE", @"Title for the 'backup export complete' alert."); NSString *message = [NSString From b77382f99c102d4f733944ea425fbcc5dc2b28d5 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 4 Jan 2018 18:16:30 -0500 Subject: [PATCH 05/15] Fix security issues in the backup process. --- Signal/src/util/OWSBackup.m | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/Signal/src/util/OWSBackup.m b/Signal/src/util/OWSBackup.m index 5ac9821642..640f21bf99 100644 --- a/Signal/src/util/OWSBackup.m +++ b/Signal/src/util/OWSBackup.m @@ -214,6 +214,9 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(backupDirPath.length > 0); NSString *filePath = [backupDirPath stringByAppendingPathComponent:fileName]; + + DDLogVerbose(@"%@ writeData: %@", self.logTag, filePath); + NSError *error; BOOL success = [data writeToFile:filePath options:NSDataWritingAtomic error:&error]; if (!success || error) { @@ -231,12 +234,32 @@ NS_ASSUME_NONNULL_BEGIN NSString *dstDirPath = [backupDirPath stringByAppendingPathComponent:dstDirName]; + DDLogVerbose(@"%@ copyDirectory: %@ -> %@", self.logTag, srcDirPath, dstDirPath); + + // We "manually" copy the "root" items in the src directory. + // Can't just use [NSFileManager copyItemAtPath:...] because the shared data container + // contains files that the app is not allowed to access. + [OWSFileSystem ensureDirectoryExists:dstDirPath]; 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); + NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:srcDirPath error:&error]; + if (error) { + OWSFail(@"%@ failed to list directory: %@, %@", self.logTag, srcDirPath, error); return NO; } + for (NSString *fileName in fileNames) { + NSString *srcFilePath = [srcDirPath stringByAppendingPathComponent:fileName]; + NSString *dstFilePath = [dstDirPath stringByAppendingPathComponent:fileName]; + if ([fileName hasPrefix:@"."]) { + DDLogVerbose(@"%@ ignoring: %@", self.logTag, srcFilePath); + continue; + } + BOOL success = [[NSFileManager defaultManager] copyItemAtPath:srcFilePath toPath:dstFilePath error:&error]; + if (!success || error) { + OWSFail(@"%@ failed to copy directory item: %@, %@", self.logTag, srcFilePath, error); + return NO; + } + } + return YES; } @@ -248,6 +271,8 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(fileName.length > 0); OWSAssert(backupDirPath.length > 0); + DDLogVerbose(@"%@ writeUserDefaults: %@", self.logTag, fileName); + NSDictionary *dictionary = userDefaults.dictionaryRepresentation; if (!dictionary) { OWSFail(@"%@ failed to extract user defaults", self.logTag); From cb4b44b8f41f368ee68cdc1456470e3f1f6e65c8 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 4 Jan 2018 18:17:53 -0500 Subject: [PATCH 06/15] Lock databases during backup export. --- Signal/src/util/OWSBackup.m | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/Signal/src/util/OWSBackup.m b/Signal/src/util/OWSBackup.m index 640f21bf99..e011e5cbd2 100644 --- a/Signal/src/util/OWSBackup.m +++ b/Signal/src/util/OWSBackup.m @@ -188,16 +188,22 @@ NS_ASSUME_NONNULL_BEGIN backupDirPath:backupDirPath]) { return; } - if (![self copyDirectory:OWSFileSystem.appDocumentDirectoryPath - dstDirName:@"appDocumentDirectoryPath" - backupDirPath:backupDirPath]) { - return; - } - if (![self copyDirectory:OWSFileSystem.appSharedDataDirectoryPath - dstDirName:@"appSharedDataDirectoryPath" - backupDirPath:backupDirPath]) { - return; - } + // Use a read/write transaction to acquire a file lock on the database files. + // + // TODO: If we use multiple database files, lock them too. + [TSStorageManager.sharedManager.newDatabaseConnection + readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + 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; } From 980b3d25a7204f712ecbb54a709649ca3ad3aa98 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 5 Jan 2018 14:42:50 -0500 Subject: [PATCH 07/15] Rework "export backup" UI. --- Signal.xcodeproj/project.pbxproj | 12 +- .../src/ViewControllers/DebugUI/DebugUIMisc.m | 21 +- .../ViewControllers/OWSBackupViewController.h | 19 ++ .../ViewControllers/OWSBackupViewController.m | 269 ++++++++++++++++++ Signal/src/util/OWSBackup.h | 31 +- Signal/src/util/OWSBackup.m | 166 +++++------ .../translations/en.lproj/Localizable.strings | 32 ++- .../ViewControllers/OWSTableViewController.h | 11 +- .../ViewControllers/OWSTableViewController.m | 44 ++- SignalMessaging/categories/UIView+OWS.h | 9 +- SignalMessaging/categories/UIView+OWS.m | 32 ++- SignalServiceKit/src/Util/MIMETypeUtil.m | 6 +- 12 files changed, 526 insertions(+), 126 deletions(-) create mode 100644 Signal/src/ViewControllers/OWSBackupViewController.h create mode 100644 Signal/src/ViewControllers/OWSBackupViewController.m diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 9133a7533e..3a12f9332c 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -42,6 +42,7 @@ 344F248720069ECB00CFB4F4 /* ModalActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344F248620069ECB00CFB4F4 /* ModalActivityIndicatorViewController.swift */; }; 344F248A20069F0600CFB4F4 /* ViewControllerUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 344F248820069F0600CFB4F4 /* ViewControllerUtils.h */; }; 344F248B20069F0600CFB4F4 /* ViewControllerUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 344F248920069F0600CFB4F4 /* ViewControllerUtils.m */; }; + 3456D2C41FFFCC70001EA55D /* OWSBackupViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3456D2C21FFFCC6F001EA55D /* OWSBackupViewController.m */; }; 3461284B1FD0B94000532771 /* SAELoadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3461284A1FD0B93F00532771 /* SAELoadViewController.swift */; }; 346129341FD1A88700532771 /* OWSSwiftUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346129331FD1A88700532771 /* OWSSwiftUtils.swift */; }; 346129391FD1B47300532771 /* OWSPreferences.h in Headers */ = {isa = PBXBuildFile; fileRef = 346129371FD1B47200532771 /* OWSPreferences.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -523,6 +524,8 @@ 344F248920069F0600CFB4F4 /* ViewControllerUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ViewControllerUtils.m; path = SignalMessaging/contacts/ViewControllerUtils.m; sourceTree = SOURCE_ROOT; }; 34533F161EA8D2070006114F /* OWSAudioAttachmentPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAudioAttachmentPlayer.h; sourceTree = ""; }; 34533F171EA8D2070006114F /* OWSAudioAttachmentPlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAudioAttachmentPlayer.m; sourceTree = ""; }; + 3456D2C21FFFCC6F001EA55D /* OWSBackupViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupViewController.m; sourceTree = ""; }; + 3456D2C31FFFCC6F001EA55D /* OWSBackupViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupViewController.h; sourceTree = ""; }; 3461284A1FD0B93F00532771 /* SAELoadViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAELoadViewController.swift; sourceTree = ""; }; 346129331FD1A88700532771 /* OWSSwiftUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSSwiftUtils.swift; sourceTree = ""; }; 346129371FD1B47200532771 /* OWSPreferences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSPreferences.h; sourceTree = ""; }; @@ -1323,6 +1326,8 @@ 34B3F8591E8DF1700035BE1A /* NotificationSettingsViewController.m */, 34CCAF391F0C2748004084F4 /* OWSAddToContactViewController.h */, 34CCAF3A1F0C2748004084F4 /* OWSAddToContactViewController.m */, + 3456D2C31FFFCC6F001EA55D /* OWSBackupViewController.h */, + 3456D2C21FFFCC6F001EA55D /* OWSBackupViewController.m */, 34B3F85B1E8DF1700035BE1A /* OWSConversationSettingsViewController.h */, 34B3F85C1E8DF1700035BE1A /* OWSConversationSettingsViewController.m */, 34D5CCAB1EAE7136005515DB /* OWSConversationSettingsViewDelegate.h */, @@ -2923,6 +2928,7 @@ 45F170BB1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */, 34B3F8711E8DF1700035BE1A /* AboutTableViewController.m in Sources */, 34B3F88D1E8DF1700035BE1A /* OWSQRCodeScanningViewController.m in Sources */, + 3456D2C41FFFCC70001EA55D /* OWSBackupViewController.m in Sources */, 34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */, 34CCAF3B1F0C2748004084F4 /* OWSAddToContactViewController.m in Sources */, 34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */, @@ -3114,7 +3120,11 @@ "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 b915111c3e..aff68bf7f3 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m @@ -4,6 +4,7 @@ #import "DebugUIMisc.h" #import "OWSBackup.h" +#import "OWSBackupViewController.h" #import "OWSCountryMetadata.h" #import "OWSTableViewController.h" #import "RegistrationViewController.h" @@ -94,10 +95,22 @@ NS_ASSUME_NONNULL_BEGIN [DebugUIMisc sendUnencryptedDatabase:thread]; }]]; } - [items addObject:[OWSTableItem itemWithTitle:@"Export Backup" - actionBlock:^{ - [OWSBackup exportBackup]; - }]]; + [items addObject:[OWSTableItem + subPageItemWithText:@"Export Backup w/ Password" + actionBlock:^(UIViewController *viewController) { + OWSBackupViewController *backupViewController = [OWSBackupViewController new]; + [backupViewController exportBackup:thread skipPassword:NO]; + [viewController.navigationController pushViewController:backupViewController + animated:YES]; + }]]; + [items addObject:[OWSTableItem + subPageItemWithText:@"Export Backup w/o Password" + actionBlock:^(UIViewController *viewController) { + OWSBackupViewController *backupViewController = [OWSBackupViewController new]; + [backupViewController exportBackup:thread skipPassword:YES]; + [viewController.navigationController pushViewController:backupViewController + animated:YES]; + }]]; return [OWSTableSection sectionWithTitle:self.name items:items]; } diff --git a/Signal/src/ViewControllers/OWSBackupViewController.h b/Signal/src/ViewControllers/OWSBackupViewController.h new file mode 100644 index 0000000000..f4467ac81a --- /dev/null +++ b/Signal/src/ViewControllers/OWSBackupViewController.h @@ -0,0 +1,19 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class TSThread; + +@interface OWSBackupViewController : OWSViewController + +// If currentThread is non-nil, we should offer to let users send the +// backup in that thread. +- (void)exportBackup:(TSThread *_Nullable)currentThread skipPassword:(BOOL)skipPassword; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/OWSBackupViewController.m b/Signal/src/ViewControllers/OWSBackupViewController.m new file mode 100644 index 0000000000..5bf5089750 --- /dev/null +++ b/Signal/src/ViewControllers/OWSBackupViewController.m @@ -0,0 +1,269 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSBackupViewController.h" +#import "OWSBackup.h" +#import "Signal-Swift.h" +#import "ThreadUtil.h" +#import +#import +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSBackupViewController () + +@property (nonatomic) OWSBackup *backup; + +@end + +#pragma mark - + +@implementation OWSBackupViewController + +- (void)loadView +{ + [super loadView]; + + self.view.backgroundColor = [UIColor whiteColor]; + + self.navigationItem.title = NSLocalizedString(@"BACKUP_EXPORT_VIEW_TITLE", @"Title for the 'backup export' view."); + self.navigationItem.leftBarButtonItem = + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemStop + target:self + action:@selector(dismissWasPressed:)]; + + self.backup.delegate = self; + + [self updateUI]; +} + +- (void)exportBackup:(TSThread *_Nullable)currentThread skipPassword:(BOOL)skipPassword +{ + OWSAssertIsOnMainThread(); + + // We set ourselves as the delegate of the backup later, + // after we've loaded our view. + self.backup = [OWSBackup new]; + [self.backup exportBackup:currentThread skipPassword:skipPassword]; +} + +- (void)updateUI +{ + for (UIView *subview in self.view.subviews) { + [subview removeFromSuperview]; + } + + switch (self.backup.backupState) { + case OWSBackupState_InProgress: + [self showInProgressUI]; + break; + case OWSBackupState_Cancelled: + [self showCancelledUI]; + break; + case OWSBackupState_Complete: + [self showCompleteUI]; + break; + } +} + +- (void)showInProgressUI +{ + + UIView *container = [UIView new]; + [self.view addSubview:container]; + [container autoCenterInSuperview]; + + UIActivityIndicatorView *activityIndicatorView = + [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + [container addSubview:activityIndicatorView]; + [activityIndicatorView autoPinEdgeToSuperviewEdge:ALEdgeTop]; + [activityIndicatorView autoHCenterInSuperview]; + [activityIndicatorView startAnimating]; + + UILabel *label = [UILabel new]; + label.text = NSLocalizedString( + @"BACKUP_EXPORT_IN_PROGRESS_MESSAGE", @"Message indicating that backup export is in progress."); + label.textColor = [UIColor blackColor]; + label.font = [UIFont ows_regularFontWithSize:18.f]; + [container addSubview:label]; + [label autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:activityIndicatorView withOffset:20.f]; + [label autoPinEdgeToSuperviewEdge:ALEdgeBottom]; + [label autoPinWidthToSuperview]; +} + +- (void)showCancelledUI +{ + // Show nothing. +} + +- (void)showCompleteUI +{ + NSMutableArray *subviews = [NSMutableArray new]; + + { + NSString *message = NSLocalizedString( + @"BACKUP_EXPORT_COMPLETE_MESSAGE", @"Message indicating that backup export without password is complete."); + + UILabel *label = [UILabel new]; + label.text = message; + label.textColor = [UIColor blackColor]; + label.font = [UIFont ows_regularFontWithSize:18.f]; + label.textAlignment = NSTextAlignmentCenter; + label.numberOfLines = 0; + label.lineBreakMode = NSLineBreakByWordWrapping; + [subviews addObject:label]; + } + + if (self.backup.backupPassword) { + NSString *message = [NSString stringWithFormat:NSLocalizedString(@"BACKUP_EXPORT_PASSWORD_MESSAGE_FORMAT", + @"Format for message indicating that backup export with " + @"password is complete. Embeds: {{the backup password}}."), + self.backup.backupPassword]; + + UILabel *label = [UILabel new]; + label.text = message; + label.textColor = [UIColor blackColor]; + label.font = [UIFont ows_regularFontWithSize:14.f]; + label.textAlignment = NSTextAlignmentCenter; + label.numberOfLines = 0; + label.lineBreakMode = NSLineBreakByWordWrapping; + [subviews addObject:label]; + } + + [subviews addObject:[UIView new]]; + + if (self.backup.backupPassword) { + [subviews + addObject:[self makeButtonWithTitle:NSLocalizedString(@"BACKUP_EXPORT_COPY_PASSWORD_BUTTON", + @"Label for button that copies backup password to the pasteboard.") + selector:@selector(copyPassword)]]; + } + + [subviews addObject:[self makeButtonWithTitle:NSLocalizedString(@"BACKUP_EXPORT_SHARE_BACKUP_BUTTON", + @"Label for button that opens share UI for backup.") + selector:@selector(shareBackup)]]; + + if (self.backup.currentThread) { + [subviews + addObject:[self makeButtonWithTitle:NSLocalizedString(@"BACKUP_EXPORT_SEND_BACKUP_BUTTON", + @"Label for button that 'send backup' in the current conversation.") + selector:@selector(sendBackup)]]; + } + + // TODO: We should offer the option to save the backup to "Files", iCloud, Dropbox, etc. + + UIView *container = [UIView verticalStackWithSubviews:subviews spacing:10]; + [self.view addSubview:container]; + [container autoVCenterInSuperview]; + [container autoPinWidthToSuperviewWithMargin:25.f]; +} + +- (UIView *)makeButtonWithTitle:(NSString *)title selector:(SEL)selector +{ + const CGFloat kButtonHeight = 40; + OWSFlatButton *button = [OWSFlatButton buttonWithTitle:title + font:[OWSFlatButton fontForHeight:kButtonHeight] + titleColor:[UIColor whiteColor] + backgroundColor:[UIColor ows_materialBlueColor] + target:self + selector:selector]; + [button autoSetDimension:ALDimensionWidth toSize:140]; + [button autoSetDimension:ALDimensionHeight toSize:kButtonHeight]; + return button; +} + +- (void)copyPassword +{ + OWSAssert(self.backup.backupPassword.length > 0); + + [UIPasteboard.generalPasteboard setString:self.backup.backupPassword]; +} + +- (void)shareBackup +{ + OWSAssertIsOnMainThread(); + OWSAssert(self.backup.backupZipPath.length > 0); + + [AttachmentSharing showShareUIForURL:[NSURL fileURLWithPath:self.backup.backupZipPath]]; +} + +- (void)sendBackup +{ + OWSAssertIsOnMainThread(); + OWSAssert(self.backup.backupZipPath.length > 0); + OWSAssert(self.backup.currentThread); + + [ModalActivityIndicatorViewController + presentFromViewController:self + canCancel:NO + backgroundBlock:^(ModalActivityIndicatorViewController *modalActivityIndicator) { + NSString *fileName = [self.backup.backupZipPath lastPathComponent]; + + OWSMessageSender *messageSender = [Environment current].messageSender; + NSString *utiType = [MIMETypeUtil utiTypeForFileExtension:fileName.pathExtension]; + DataSource *_Nullable dataSource = + [DataSourcePath dataSourceWithFilePath:self.backup.backupZipPath]; + [dataSource setSourceFilename:fileName]; + SignalAttachment *attachment = + [SignalAttachment attachmentWithDataSource:dataSource dataUTI:utiType]; + if (!attachment || [attachment hasError]) { + OWSFail(@"%@ attachment[%@]: %@", + self.logTag, + [attachment sourceFilename], + [attachment errorName]); + return; + } + dispatch_async(dispatch_get_main_queue(), ^{ + [ThreadUtil + sendMessageWithAttachment:attachment + inThread:self.backup.currentThread + messageSender:messageSender + completion:^(NSError *_Nullable error) { + + OWSAssertIsOnMainThread(); + [modalActivityIndicator dismissWithCompletion:^{ + if (error) { + DDLogError(@"%@ send backup failed: %@", self.logTag, error); + [OWSAlerts + showAlertWithTitle:NSLocalizedString( + @"BACKUP_EXPORT_SEND_BACKUP_FAILED", + @"Message indicating that sending " + @"the backup failed.")]; + } else { + [OWSAlerts + showAlertWithTitle: + NSLocalizedString(@"BACKUP_EXPORT_SEND_BACKUP_SUCCESS", + @"Message indicating that sending the backup " + @"succeeded.")]; + } + }]; + }]; + }); + }]; +} + +- (void)dismissWasPressed:(id)sender +{ + [self.backup cancel]; + + [self.navigationController popViewControllerAnimated:YES]; +} + +#pragma mark - OWSBackupDelegate + +- (void)backupStateDidChange +{ + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + + [self updateUI]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/util/OWSBackup.h b/Signal/src/util/OWSBackup.h index 3c28653e0f..c9e103b66e 100644 --- a/Signal/src/util/OWSBackup.h +++ b/Signal/src/util/OWSBackup.h @@ -4,9 +4,38 @@ NS_ASSUME_NONNULL_BEGIN +@protocol OWSBackupDelegate + +- (void)backupStateDidChange; + +@end + +#pragma mark - + +typedef NS_ENUM(NSUInteger, OWSBackupState) { + OWSBackupState_InProgress, + OWSBackupState_Cancelled, + OWSBackupState_Complete, +}; + +@class TSThread; + @interface OWSBackup : NSObject -+ (void)exportBackup; +@property (nonatomic, weak) id delegate; + +@property (nonatomic) OWSBackupState backupState; + +// If non-nil, backup is encrypted. +@property (nonatomic, nullable, readonly) NSString *backupPassword; + +@property (nonatomic, nullable, readonly) TSThread *currentThread; + +@property (nonatomic, readonly) NSString *backupZipPath; + +- (void)exportBackup:(nullable TSThread *)currentThread skipPassword:(BOOL)skipPassword; + +- (void)cancel; @end diff --git a/Signal/src/util/OWSBackup.m b/Signal/src/util/OWSBackup.m index e011e5cbd2..06d5dba525 100644 --- a/Signal/src/util/OWSBackup.m +++ b/Signal/src/util/OWSBackup.m @@ -7,7 +7,6 @@ #import "Signal-Swift.h" #import "zlib.h" #import -#import #import #import #import @@ -24,10 +23,12 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSBackup () -@property (nonatomic) NSString *password; +@property (nonatomic, nullable) TSThread *currentThread; + +@property (nonatomic, nullable) NSString *backupPassword; + @property (nonatomic) NSString *backupDirPath; @property (nonatomic) NSString *backupZipPath; -@property (atomic) BOOL cancelled; @end @@ -43,107 +44,58 @@ NS_ASSUME_NONNULL_BEGIN [OWSFileSystem deleteFileIfExists:self.backupDirPath]; } -+ (void)exportBackup +- (void)setBackupState:(OWSBackupState)backupState { - dispatch_async(dispatch_get_main_queue(), ^{ - [[OWSBackup new] exportBackup]; - }); + _backupState = backupState; + + [self.delegate backupStateDidChange]; } -- (void)exportBackup +- (void)cancel +{ + self.backupState = OWSBackupState_Cancelled; +} + +- (BOOL)isCancelled +{ + return self.backupState == OWSBackupState_Cancelled; +} + +- (void)exportBackup:(nullable TSThread *)currentThread skipPassword:(BOOL)skipPassword { OWSAssertIsOnMainThread(); - // TODO: Should the user pick a password? - // If not, should probably generate something more user-friendly, - // e.g. case-insensitive set of hexadecimal? - NSString *password = [NSUUID UUID].UUIDString; - self.password = password; - DDLogVerbose(@"%@ backup export complete; password: %@", self.logTag, password); + self.currentThread = currentThread; + self.backupState = OWSBackupState_InProgress; - [self showExportProgressUI:^(UIAlertController *exportProgressAlert) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [self exportToFilesAndZip]; + if (skipPassword) { + DDLogVerbose(@"%@ backup export without password", self.logTag); + } else { + // TODO: Should the user pick a password? + // If not, should probably generate something more user-friendly, + // e.g. case-insensitive set of hexadecimal? + NSString *backupPassword = [NSUUID UUID].UUIDString; + self.backupPassword = backupPassword; + DDLogVerbose(@"%@ backup export with password: %@", self.logTag, backupPassword); + } - dispatch_async(dispatch_get_main_queue(), ^{ - [exportProgressAlert dismissViewControllerAnimated:YES - completion:^{ - [self showExportCompleteUI:^{ - [self showShareUI]; - }]; - }]; - }); + [self startExport]; +} + +- (void)startExport +{ + OWSAssertIsOnMainThread(); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self exportToFilesAndZip]; + + dispatch_async(dispatch_get_main_queue(), ^{ + if (!self.isCancelled) { + self.backupState = OWSBackupState_Complete; + } + [self.delegate backupStateDidChange]; }); - }]; -} - -- (void)showExportProgressUI:(void (^_Nonnull)(UIAlertController *))completion -{ - OWSAssertIsOnMainThread(); - OWSAssert(completion); - - NSString *title = NSLocalizedString( - @"BACKUP_EXPORT_IN_PROGRESS_ALERT_TITLE", @"Title for the 'backup export in progress' alert."); - UIAlertController *alert = - [UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:[CommonStrings cancelButton] - style:UIAlertActionStyleCancel - handler:^(UIAlertAction *_Nonnull action) { - self.cancelled = YES; - }]; - [alert addAction:cancelAction]; - - __weak UIAlertController *weakAlert = alert; - UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController]; - [fromViewController presentViewController:alert - animated:YES - completion:^(void) { - UIAlertController *strongAlert = weakAlert; - if (!strongAlert) { - OWSFail(@"%@ missing alert.", self.logTag); - return; - } - completion(strongAlert); - }]; -} - -- (void)showExportCompleteUI:(void (^_Nonnull)(void))completion -{ - OWSAssertIsOnMainThread(); - OWSAssert(completion); - OWSAssert(self.password.length > 0); - - // TODO: We probably want to offer an option that lets users copy - // the password to the pasteboard. - NSString *title - = NSLocalizedString(@"BACKUP_EXPORT_COMPLETE_ALERT_TITLE", @"Title for the 'backup export complete' alert."); - NSString *message = [NSString - stringWithFormat: - NSLocalizedString(@"BACKUP_EXPORT_COMPLETE_ALERT_MESSAGE_FORMAT", - @"Format for message for the 'backup export complete' alert. Embeds: {{the backup password}}."), - self.password]; - UIAlertController *alert = - [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction *okAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", comment - : nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *_Nonnull action) { - completion(); - }]; - [alert addAction:okAction]; - - UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController]; - [fromViewController presentViewController:alert animated:YES completion:nil]; -} - -- (void)showShareUI -{ - OWSAssertIsOnMainThread(); - OWSAssert(self.backupZipPath.length > 0); - - [AttachmentSharing showShareUIForURL:[NSURL fileURLWithPath:self.backupZipPath]]; + }); } - (void)exportToFilesAndZip @@ -173,21 +125,34 @@ NS_ASSUME_NONNULL_BEGIN [OWSFileSystem protectFolderAtPath:rootDirPath]; [OWSFileSystem ensureDirectoryExists:backupDirPath]; + if (self.isCancelled) { + return; + } + NSData *databasePassword = [TSStorageManager sharedManager].databasePassword; if (![self writeData:databasePassword fileName:@"databasePassword" backupDirPath:backupDirPath]) { return; } + if (self.isCancelled) { + return; + } if (![self writeUserDefaults:NSUserDefaults.standardUserDefaults fileName:@"standardUserDefaults" backupDirPath:backupDirPath]) { return; } + if (self.isCancelled) { + return; + } if (![self writeUserDefaults:NSUserDefaults.appUserDefaults fileName:@"appUserDefaults" backupDirPath:backupDirPath]) { return; } + if (self.isCancelled) { + return; + } // Use a read/write transaction to acquire a file lock on the database files. // // TODO: If we use multiple database files, lock them too. @@ -198,12 +163,18 @@ NS_ASSUME_NONNULL_BEGIN backupDirPath:backupDirPath]) { return; } + if (self.isCancelled) { + return; + } if (![self copyDirectory:OWSFileSystem.appSharedDataDirectoryPath dstDirName:@"appSharedDataDirectoryPath" backupDirPath:backupDirPath]) { return; } }]; + if (self.isCancelled) { + return; + } if (![self zipDirectory:backupDirPath dstFilePath:backupZipPath]) { return; } @@ -297,14 +268,13 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssert(srcDirPath.length > 0); OWSAssert(dstFilePath.length > 0); - OWSAssert(self.password.length > 0); BOOL success = [SSZipArchive createZipFileAtPath:dstFilePath withContentsOfDirectory:srcDirPath keepParentDirectory:NO compressionLevel:Z_DEFAULT_COMPRESSION - password:self.password - AES:YES + password:self.backupPassword + AES:self.backupPassword != nil progressHandler:^(NSUInteger entryNumber, NSUInteger total) { DDLogVerbose(@"%@ Zip progress: %zd / %zd = %f", self.logTag, diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 86faee4f01..414abd6887 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -145,16 +145,34 @@ /* action sheet button title to enable built in speaker during a call */ "AUDIO_ROUTE_BUILT_IN_SPEAKER" = "Speaker"; -/* Format for message for the 'backup export complete' alert. Embeds: {{the backup password}} */ -"BACKUP_EXPORT_COMPLETE_ALERT_MESSAGE_FORMAT" = "Your backup password is: %@. Make sure to keep a copy of this password or you won't be able to restore from this backup."; +/* Message indicating that backup export without password is complete. */ +"BACKUP_EXPORT_COMPLETE_MESSAGE" = "Backup complete."; -/* Title for the 'backup export complete' alert. */ -"BACKUP_EXPORT_COMPLETE_ALERT_TITLE" = "Backup Complete"; +/* Label for button that copies backup password to the pasteboard. */ +"BACKUP_EXPORT_COPY_PASSWORD_BUTTON" = "Copy Password"; -/* Title for the 'backup export in progress' alert. */ -"BACKUP_EXPORT_IN_PROGRESS_ALERT_TITLE" = "Exporting Backup..."; +/* Message indicating that backup export is in progress. */ +"BACKUP_EXPORT_IN_PROGRESS_MESSAGE" = "Exporting Backup..."; -/* Format for backup filenames. Embeds: {{the date and time of the backup}} */ +/* Format for message indicating that backup export with password is complete. Embeds: {{the backup password}}. */ +"BACKUP_EXPORT_PASSWORD_MESSAGE_FORMAT" = "Your backup password is: %@. Make sure to keep a copy of this password or you won't be able to restore from this backup."; + +/* Label for button that 'send backup' in the current conversation. */ +"BACKUP_EXPORT_SEND_BACKUP_BUTTON" = "Send Backup as Message"; + +/* Message indicating that sending the backup failed. */ +"BACKUP_EXPORT_SEND_BACKUP_FAILED" = "Sending Backup Failed."; + +/* Message indicating that sending the backup succeeded. */ +"BACKUP_EXPORT_SEND_BACKUP_SUCCESS" = "Backup Sent."; + +/* Label for button that opens share UI for backup. */ +"BACKUP_EXPORT_SHARE_BACKUP_BUTTON" = "Share Backup"; + +/* Title for the 'backup export' view. */ +"BACKUP_EXPORT_VIEW_TITLE" = "Backup"; + +/* Format for backup filenames. Embeds: {{the date and time of the backup}}. Should not include characters like slash (/ or \\) or colon (:). */ "BACKUP_FILENAME_FORMAT" = "Signal Backup %@"; /* An explanation of the consequences of blocking another user. */ diff --git a/SignalMessaging/ViewControllers/OWSTableViewController.h b/SignalMessaging/ViewControllers/OWSTableViewController.h index b6c97efe09..e1c67e170c 100644 --- a/SignalMessaging/ViewControllers/OWSTableViewController.h +++ b/SignalMessaging/ViewControllers/OWSTableViewController.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "OWSViewController.h" @@ -50,10 +50,13 @@ typedef NS_ENUM(NSInteger, OWSTableItemType) { }; typedef void (^OWSTableActionBlock)(void); +typedef void (^OWSTableSubPageBlock)(UIViewController *viewController); typedef UITableViewCell *_Nonnull (^OWSTableCustomCellBlock)(void); @interface OWSTableItem : NSObject +@property (nonatomic, weak) UIViewController *tableViewController; + + (OWSTableItem *)itemWithTitle:(NSString *)title actionBlock:(nullable OWSTableActionBlock)actionBlock; + (OWSTableItem *)itemWithCustomCell:(UITableViewCell *)customCell @@ -73,6 +76,12 @@ typedef UITableViewCell *_Nonnull (^OWSTableCustomCellBlock)(void); customRowHeight:(CGFloat)customRowHeight actionBlock:(nullable OWSTableActionBlock)actionBlock; ++ (OWSTableItem *)subPageItemWithText:(NSString *)text actionBlock:(nullable OWSTableSubPageBlock)actionBlock; + ++ (OWSTableItem *)subPageItemWithText:(NSString *)text + customRowHeight:(CGFloat)customRowHeight + actionBlock:(nullable OWSTableSubPageBlock)actionBlock; + + (OWSTableItem *)actionItemWithText:(NSString *)text actionBlock:(nullable OWSTableActionBlock)actionBlock; + (OWSTableItem *)softCenterLabelItemWithText:(NSString *)text; diff --git a/SignalMessaging/ViewControllers/OWSTableViewController.m b/SignalMessaging/ViewControllers/OWSTableViewController.m index 0633492888..1575659562 100644 --- a/SignalMessaging/ViewControllers/OWSTableViewController.m +++ b/SignalMessaging/ViewControllers/OWSTableViewController.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "OWSTableViewController.h" @@ -153,6 +153,7 @@ const CGFloat kOWSTable_DefaultCellHeight = 45.f; OWSTableItem *item = [OWSTableItem new]; item.itemType = OWSTableItemTypeAction; + __weak OWSTableItem *weakItem = item; item.actionBlock = actionBlock; item.customCellBlock = ^{ UITableViewCell *cell = [UITableViewCell new]; @@ -176,6 +177,45 @@ const CGFloat kOWSTable_DefaultCellHeight = 45.f; return item; } ++ (OWSTableItem *)subPageItemWithText:(NSString *)text actionBlock:(nullable OWSTableSubPageBlock)actionBlock +{ + OWSAssert(text.length > 0); + OWSAssert(actionBlock); + + OWSTableItem *item = [OWSTableItem new]; + item.itemType = OWSTableItemTypeAction; + __weak OWSTableItem *weakItem = item; + item.actionBlock = ^{ + OWSTableItem *strongItem = weakItem; + OWSAssert(strongItem); + OWSAssert(strongItem.tableViewController); + + if (actionBlock) { + actionBlock(strongItem.tableViewController); + } + }; + item.customCellBlock = ^{ + UITableViewCell *cell = [UITableViewCell new]; + cell.textLabel.text = text; + cell.textLabel.font = [UIFont ows_regularFontWithSize:18.f]; + cell.textLabel.textColor = [UIColor blackColor]; + cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; + return cell; + }; + return item; +} + ++ (OWSTableItem *)subPageItemWithText:(NSString *)text + customRowHeight:(CGFloat)customRowHeight + actionBlock:(nullable OWSTableSubPageBlock)actionBlock +{ + OWSAssert(customRowHeight > 0); + + OWSTableItem *item = [self subPageItemWithText:text actionBlock:actionBlock]; + item.customRowHeight = @(customRowHeight); + return item; +} + + (OWSTableItem *)actionItemWithText:(NSString *)text actionBlock:(nullable OWSTableActionBlock)actionBlock { OWSAssert(text.length > 0); @@ -481,6 +521,8 @@ NSString *const kOWSTableCellIdentifier = @"kOWSTableCellIdentifier"; { OWSTableItem *item = [self itemForIndexPath:indexPath]; + item.tableViewController = self; + UITableViewCell *customCell = [item customCell]; if (customCell) { return customCell; diff --git a/SignalMessaging/categories/UIView+OWS.h b/SignalMessaging/categories/UIView+OWS.h index d8287e78b8..3951e219b3 100644 --- a/SignalMessaging/categories/UIView+OWS.h +++ b/SignalMessaging/categories/UIView+OWS.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import @@ -95,9 +95,14 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value); - (NSTextAlignment)textAlignmentUnnatural; // Leading and trailing anchors honor layout margins. // When using a UIView as a "div" to structure layout, we don't want it to have margins. -+ (UIView *)containerView; - (void)setHLayoutMargins:(CGFloat)value; +#pragma mark - Containers + ++ (UIView *)containerView; + ++ (UIView *)verticalStackWithSubviews:(NSArray *)subviews spacing:(int)spacing; + #pragma mark - Debugging - (void)addBorderWithColor:(UIColor *)color; diff --git a/SignalMessaging/categories/UIView+OWS.m b/SignalMessaging/categories/UIView+OWS.m index ffbfbaa1a2..f25a0eec02 100644 --- a/SignalMessaging/categories/UIView+OWS.m +++ b/SignalMessaging/categories/UIView+OWS.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "OWSMath.h" @@ -388,6 +388,16 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value) return (self.isRTL ? NSTextAlignmentLeft : NSTextAlignmentRight); } +- (void)setHLayoutMargins:(CGFloat)value +{ + UIEdgeInsets layoutMargins = self.layoutMargins; + layoutMargins.left = value; + layoutMargins.right = value; + self.layoutMargins = layoutMargins; +} + +#pragma mark - Containers + + (UIView *)containerView { UIView *view = [UIView new]; @@ -397,12 +407,22 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value) return view; } -- (void)setHLayoutMargins:(CGFloat)value ++ (UIView *)verticalStackWithSubviews:(NSArray *)subviews spacing:(int)spacing { - UIEdgeInsets layoutMargins = self.layoutMargins; - layoutMargins.left = value; - layoutMargins.right = value; - self.layoutMargins = layoutMargins; + UIView *container = [UIView containerView]; + UIView *_Nullable lastSubview = nil; + for (UIView *subview in subviews) { + [container addSubview:subview]; + [subview autoPinWidthToSuperview]; + if (lastSubview) { + [subview autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:lastSubview withOffset:spacing]; + } else { + [subview autoPinEdgeToSuperviewEdge:ALEdgeTop]; + } + lastSubview = subview; + } + [lastSubview autoPinEdgeToSuperviewEdge:ALEdgeBottom]; + return container; } #pragma mark - Debugging diff --git a/SignalServiceKit/src/Util/MIMETypeUtil.m b/SignalServiceKit/src/Util/MIMETypeUtil.m index 1f62471c31..cf26dd21f6 100644 --- a/SignalServiceKit/src/Util/MIMETypeUtil.m +++ b/SignalServiceKit/src/Util/MIMETypeUtil.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "MIMETypeUtil.h" @@ -461,7 +461,6 @@ NSString *const kSyncMessageFileExtension = @"bin"; + (NSSet *)supportedVideoUTITypes { - OWSAssertIsOnMainThread(); static NSSet *result = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -472,7 +471,6 @@ NSString *const kSyncMessageFileExtension = @"bin"; + (NSSet *)supportedAudioUTITypes { - OWSAssertIsOnMainThread(); static NSSet *result = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -483,7 +481,6 @@ NSString *const kSyncMessageFileExtension = @"bin"; + (NSSet *)supportedImageUTITypes { - OWSAssertIsOnMainThread(); static NSSet *result = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -494,7 +491,6 @@ NSString *const kSyncMessageFileExtension = @"bin"; + (NSSet *)supportedAnimatedImageUTITypes { - OWSAssertIsOnMainThread(); static NSSet *result = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ From 857ca56ab6bf470ebc2eb30467072c58c46beb2a Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 8 Jan 2018 11:01:09 -0500 Subject: [PATCH 08/15] Rework progress mode of export backup UI. --- .../ViewControllers/OWSBackupViewController.m | 79 ++++++++++++++----- Signal/src/util/OWSBackup.h | 7 +- Signal/src/util/OWSBackup.m | 40 +++++++--- 3 files changed, 97 insertions(+), 29 deletions(-) diff --git a/Signal/src/ViewControllers/OWSBackupViewController.m b/Signal/src/ViewControllers/OWSBackupViewController.m index 5bf5089750..2b3bb03c09 100644 --- a/Signal/src/ViewControllers/OWSBackupViewController.m +++ b/Signal/src/ViewControllers/OWSBackupViewController.m @@ -4,6 +4,7 @@ #import "OWSBackupViewController.h" #import "OWSBackup.h" +#import "OWSProgressView.h" #import "Signal-Swift.h" #import "ThreadUtil.h" #import @@ -20,6 +21,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) OWSBackup *backup; +@property (nonatomic, nullable) OWSProgressView *progressView; + @end #pragma mark - @@ -58,6 +61,7 @@ NS_ASSUME_NONNULL_BEGIN for (UIView *subview in self.view.subviews) { [subview removeFromSuperview]; } + self.progressView = nil; switch (self.backup.backupState) { case OWSBackupState_InProgress: @@ -69,32 +73,37 @@ NS_ASSUME_NONNULL_BEGIN case OWSBackupState_Complete: [self showCompleteUI]; break; + case OWSBackupState_Failed: + [self showFailedUI]; + break; } } - (void)showInProgressUI { - - UIView *container = [UIView new]; - [self.view addSubview:container]; - [container autoCenterInSuperview]; - - UIActivityIndicatorView *activityIndicatorView = - [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; - [container addSubview:activityIndicatorView]; - [activityIndicatorView autoPinEdgeToSuperviewEdge:ALEdgeTop]; - [activityIndicatorView autoHCenterInSuperview]; - [activityIndicatorView startAnimating]; + self.progressView = [OWSProgressView new]; + self.progressView.color = [UIColor ows_materialBlueColor]; + self.progressView.progress = self.backup.backupProgress; + [self.progressView autoSetDimension:ALDimensionWidth toSize:300]; + [self.progressView autoSetDimension:ALDimensionHeight toSize:20]; UILabel *label = [UILabel new]; label.text = NSLocalizedString( @"BACKUP_EXPORT_IN_PROGRESS_MESSAGE", @"Message indicating that backup export is in progress."); label.textColor = [UIColor blackColor]; label.font = [UIFont ows_regularFontWithSize:18.f]; - [container addSubview:label]; - [label autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:activityIndicatorView withOffset:20.f]; - [label autoPinEdgeToSuperviewEdge:ALEdgeBottom]; - [label autoPinWidthToSuperview]; + label.textAlignment = NSTextAlignmentCenter; + label.numberOfLines = 0; + label.lineBreakMode = NSLineBreakByWordWrapping; + + UIView *container = [UIView verticalStackWithSubviews:@[ + label, + self.progressView, + ] + spacing:10]; + [self.view addSubview:container]; + [container autoVCenterInSuperview]; + [container autoPinWidthToSuperviewWithMargin:25.f]; } - (void)showCancelledUI @@ -108,7 +117,7 @@ NS_ASSUME_NONNULL_BEGIN { NSString *message = NSLocalizedString( - @"BACKUP_EXPORT_COMPLETE_MESSAGE", @"Message indicating that backup export without password is complete."); + @"BACKUP_EXPORT_COMPLETE_MESSAGE", @"Message indicating that backup export is complete."); UILabel *label = [UILabel new]; label.text = message; @@ -122,8 +131,8 @@ NS_ASSUME_NONNULL_BEGIN if (self.backup.backupPassword) { NSString *message = [NSString stringWithFormat:NSLocalizedString(@"BACKUP_EXPORT_PASSWORD_MESSAGE_FORMAT", - @"Format for message indicating that backup export with " - @"password is complete. Embeds: {{the backup password}}."), + @"Format for message indicating that backup export " + @"is complete. Embeds: {{the backup password}}."), self.backup.backupPassword]; UILabel *label = [UILabel new]; @@ -164,6 +173,32 @@ NS_ASSUME_NONNULL_BEGIN [container autoPinWidthToSuperviewWithMargin:25.f]; } +- (void)showFailedUI +{ + NSMutableArray *subviews = [NSMutableArray new]; + + { + NSString *message + = NSLocalizedString(@"BACKUP_EXPORT_FAILED_MESSAGE", @"Message indicating that backup export failed."); + + UILabel *label = [UILabel new]; + label.text = message; + label.textColor = [UIColor blackColor]; + label.font = [UIFont ows_regularFontWithSize:18.f]; + label.textAlignment = NSTextAlignmentCenter; + label.numberOfLines = 0; + label.lineBreakMode = NSLineBreakByWordWrapping; + [subviews addObject:label]; + } + + // TODO: We should offer the option to save the backup to "Files", iCloud, Dropbox, etc. + + UIView *container = [UIView verticalStackWithSubviews:subviews spacing:10]; + [self.view addSubview:container]; + [container autoVCenterInSuperview]; + [container autoPinWidthToSuperviewWithMargin:25.f]; +} + - (UIView *)makeButtonWithTitle:(NSString *)title selector:(SEL)selector { const CGFloat kButtonHeight = 40; @@ -264,6 +299,14 @@ NS_ASSUME_NONNULL_BEGIN [self updateUI]; } + +- (void)backupProgressDidChange +{ + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + + self.progressView.progress = self.backup.backupProgress; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/util/OWSBackup.h b/Signal/src/util/OWSBackup.h index c9e103b66e..85d5bba943 100644 --- a/Signal/src/util/OWSBackup.h +++ b/Signal/src/util/OWSBackup.h @@ -8,6 +8,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)backupStateDidChange; +- (void)backupProgressDidChange; + @end #pragma mark - @@ -16,6 +18,7 @@ typedef NS_ENUM(NSUInteger, OWSBackupState) { OWSBackupState_InProgress, OWSBackupState_Cancelled, OWSBackupState_Complete, + OWSBackupState_Failed, }; @class TSThread; @@ -24,7 +27,9 @@ typedef NS_ENUM(NSUInteger, OWSBackupState) { @property (nonatomic, weak) id delegate; -@property (nonatomic) OWSBackupState backupState; +@property (nonatomic, readonly) OWSBackupState backupState; + +@property (nonatomic, readonly) CGFloat backupProgress; // If non-nil, backup is encrypted. @property (nonatomic, nullable, readonly) NSString *backupPassword; diff --git a/Signal/src/util/OWSBackup.m b/Signal/src/util/OWSBackup.m index 06d5dba525..376d5940a2 100644 --- a/Signal/src/util/OWSBackup.m +++ b/Signal/src/util/OWSBackup.m @@ -23,6 +23,10 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSBackup () +@property (nonatomic) OWSBackupState backupState; + +@property (nonatomic) CGFloat backupProgress; + @property (nonatomic, nullable) TSThread *currentThread; @property (nonatomic, nullable) NSString *backupPassword; @@ -51,14 +55,21 @@ NS_ASSUME_NONNULL_BEGIN [self.delegate backupStateDidChange]; } +- (void)fail +{ + if (!self.isCancelledOrFailed) { + self.backupState = OWSBackupState_Failed; + } +} + - (void)cancel { self.backupState = OWSBackupState_Cancelled; } -- (BOOL)isCancelled +- (BOOL)isCancelledOrFailed { - return self.backupState == OWSBackupState_Cancelled; + return (self.backupState == OWSBackupState_Cancelled || self.backupState == OWSBackupState_Failed); } - (void)exportBackup:(nullable TSThread *)currentThread skipPassword:(BOOL)skipPassword @@ -90,7 +101,7 @@ NS_ASSUME_NONNULL_BEGIN [self exportToFilesAndZip]; dispatch_async(dispatch_get_main_queue(), ^{ - if (!self.isCancelled) { + if (!self.isCancelledOrFailed) { self.backupState = OWSBackupState_Complete; } [self.delegate backupStateDidChange]; @@ -125,32 +136,35 @@ NS_ASSUME_NONNULL_BEGIN [OWSFileSystem protectFolderAtPath:rootDirPath]; [OWSFileSystem ensureDirectoryExists:backupDirPath]; - if (self.isCancelled) { + if (self.isCancelledOrFailed) { return; } NSData *databasePassword = [TSStorageManager sharedManager].databasePassword; if (![self writeData:databasePassword fileName:@"databasePassword" backupDirPath:backupDirPath]) { + [self fail]; return; } - if (self.isCancelled) { + if (self.isCancelledOrFailed) { return; } if (![self writeUserDefaults:NSUserDefaults.standardUserDefaults fileName:@"standardUserDefaults" backupDirPath:backupDirPath]) { + [self fail]; return; } - if (self.isCancelled) { + if (self.isCancelledOrFailed) { return; } if (![self writeUserDefaults:NSUserDefaults.appUserDefaults fileName:@"appUserDefaults" backupDirPath:backupDirPath]) { + [self fail]; return; } - if (self.isCancelled) { + if (self.isCancelledOrFailed) { return; } // Use a read/write transaction to acquire a file lock on the database files. @@ -161,21 +175,24 @@ NS_ASSUME_NONNULL_BEGIN if (![self copyDirectory:OWSFileSystem.appDocumentDirectoryPath dstDirName:@"appDocumentDirectoryPath" backupDirPath:backupDirPath]) { + [self fail]; return; } - if (self.isCancelled) { + if (self.isCancelledOrFailed) { return; } if (![self copyDirectory:OWSFileSystem.appSharedDataDirectoryPath dstDirName:@"appSharedDataDirectoryPath" backupDirPath:backupDirPath]) { + [self fail]; return; } }]; - if (self.isCancelled) { + if (self.isCancelledOrFailed) { return; } if (![self zipDirectory:backupDirPath dstFilePath:backupZipPath]) { + [self fail]; return; } @@ -281,7 +298,10 @@ NS_ASSUME_NONNULL_BEGIN entryNumber, total, entryNumber / (CGFloat)total); - // TODO: + + CGFloat progress = entryNumber / (CGFloat)total; + self.backupProgress = progress; + [self.delegate backupProgressDidChange]; }]; if (!success) { From 272a90d26944b624c403d036b4dc9cde26b114b5 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 8 Jan 2018 15:31:51 -0500 Subject: [PATCH 09/15] Add import back up UI. Begin work on import backup logic. --- Signal.xcodeproj/project.pbxproj | 17 +- .../AppSettingsViewController.m | 6 +- .../ConversationView/Cells/OWSMessageCell.m | 47 ++- .../src/ViewControllers/DebugUI/DebugUIMisc.m | 8 +- .../src/ViewControllers/HomeViewController.m | 4 + ...ller.h => OWSBackupExportViewController.h} | 2 +- ...ller.m => OWSBackupExportViewController.m} | 7 +- .../OWSBackupImportViewController.h | 17 ++ .../OWSBackupImportViewController.m | 242 +++++++++++++++ Signal/src/util/OWSBackup.h | 4 + Signal/src/util/OWSBackup.m | 286 +++++++++++++++--- .../translations/en.lproj/Localizable.strings | 40 ++- SignalServiceKit/src/Util/OWSFileSystem.h | 2 + SignalServiceKit/src/Util/OWSFileSystem.m | 31 ++ 14 files changed, 655 insertions(+), 58 deletions(-) rename Signal/src/ViewControllers/{OWSBackupViewController.h => OWSBackupExportViewController.h} (86%) rename Signal/src/ViewControllers/{OWSBackupViewController.m => OWSBackupExportViewController.m} (98%) create mode 100644 Signal/src/ViewControllers/OWSBackupImportViewController.h create mode 100644 Signal/src/ViewControllers/OWSBackupImportViewController.m diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 3a12f9332c..acf560a9f0 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -118,6 +118,8 @@ 3478506C1FD9B78A007B8332 /* NoopNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347850681FD9B78A007B8332 /* NoopNotificationsManager.swift */; }; 347850711FDAEB17007B8332 /* OWSUserProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 3478506F1FDAEB16007B8332 /* OWSUserProfile.m */; }; 347850721FDAEB17007B8332 /* OWSUserProfile.h in Headers */ = {isa = PBXBuildFile; fileRef = 347850701FDAEB16007B8332 /* OWSUserProfile.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 347E0B7A2003CD7500BC2F76 /* OWSBackupExportViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 347E0B762003CD7400BC2F76 /* OWSBackupExportViewController.m */; }; + 347E0B7B2003CD7500BC2F76 /* OWSBackupImportViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 347E0B782003CD7400BC2F76 /* OWSBackupImportViewController.m */; }; 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 */; }; @@ -524,8 +526,6 @@ 344F248920069F0600CFB4F4 /* ViewControllerUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ViewControllerUtils.m; path = SignalMessaging/contacts/ViewControllerUtils.m; sourceTree = SOURCE_ROOT; }; 34533F161EA8D2070006114F /* OWSAudioAttachmentPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAudioAttachmentPlayer.h; sourceTree = ""; }; 34533F171EA8D2070006114F /* OWSAudioAttachmentPlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAudioAttachmentPlayer.m; sourceTree = ""; }; - 3456D2C21FFFCC6F001EA55D /* OWSBackupViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupViewController.m; sourceTree = ""; }; - 3456D2C31FFFCC6F001EA55D /* OWSBackupViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupViewController.h; sourceTree = ""; }; 3461284A1FD0B93F00532771 /* SAELoadViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAELoadViewController.swift; sourceTree = ""; }; 346129331FD1A88700532771 /* OWSSwiftUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSSwiftUtils.swift; sourceTree = ""; }; 346129371FD1B47200532771 /* OWSPreferences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSPreferences.h; sourceTree = ""; }; @@ -601,6 +601,10 @@ 347850681FD9B78A007B8332 /* NoopNotificationsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoopNotificationsManager.swift; sourceTree = ""; }; 3478506F1FDAEB16007B8332 /* OWSUserProfile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSUserProfile.m; sourceTree = ""; }; 347850701FDAEB16007B8332 /* OWSUserProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSUserProfile.h; sourceTree = ""; }; + 347E0B762003CD7400BC2F76 /* OWSBackupExportViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupExportViewController.m; sourceTree = ""; }; + 347E0B772003CD7400BC2F76 /* OWSBackupExportViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupExportViewController.h; sourceTree = ""; }; + 347E0B782003CD7400BC2F76 /* OWSBackupImportViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupImportViewController.m; sourceTree = ""; }; + 347E0B792003CD7500BC2F76 /* OWSBackupImportViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupImportViewController.h; sourceTree = ""; }; 348F2EAD1F0D21BC00D4ECE0 /* DeviceSleepManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceSleepManager.swift; sourceTree = ""; }; 3495BC911F1426B800B478F5 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = translations/ar.lproj/Localizable.strings; sourceTree = ""; }; 3497DBEA1ECE257500DB2605 /* OWSCountryMetadata.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSCountryMetadata.h; sourceTree = ""; }; @@ -1326,8 +1330,10 @@ 34B3F8591E8DF1700035BE1A /* NotificationSettingsViewController.m */, 34CCAF391F0C2748004084F4 /* OWSAddToContactViewController.h */, 34CCAF3A1F0C2748004084F4 /* OWSAddToContactViewController.m */, - 3456D2C31FFFCC6F001EA55D /* OWSBackupViewController.h */, - 3456D2C21FFFCC6F001EA55D /* OWSBackupViewController.m */, + 347E0B772003CD7400BC2F76 /* OWSBackupExportViewController.h */, + 347E0B762003CD7400BC2F76 /* OWSBackupExportViewController.m */, + 347E0B792003CD7500BC2F76 /* OWSBackupImportViewController.h */, + 347E0B782003CD7400BC2F76 /* OWSBackupImportViewController.m */, 34B3F85B1E8DF1700035BE1A /* OWSConversationSettingsViewController.h */, 34B3F85C1E8DF1700035BE1A /* OWSConversationSettingsViewController.m */, 34D5CCAB1EAE7136005515DB /* OWSConversationSettingsViewDelegate.h */, @@ -2905,6 +2911,7 @@ 34D1F0B71F87F8850066283D /* OWSGenericAttachmentView.m in Sources */, 34B3F8801E8DF1700035BE1A /* InviteFlow.swift in Sources */, 34B3F8871E8DF1700035BE1A /* NotificationSettingsViewController.m in Sources */, + 347E0B7B2003CD7500BC2F76 /* OWSBackupImportViewController.m in Sources */, 458E38371D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m in Sources */, 4517642B1DE939FD00EDB8B9 /* ContactCell.swift in Sources */, 34B3F8751E8DF1700035BE1A /* CallViewController.swift in Sources */, @@ -2928,7 +2935,6 @@ 45F170BB1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */, 34B3F8711E8DF1700035BE1A /* AboutTableViewController.m in Sources */, 34B3F88D1E8DF1700035BE1A /* OWSQRCodeScanningViewController.m in Sources */, - 3456D2C41FFFCC70001EA55D /* OWSBackupViewController.m in Sources */, 34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */, 34CCAF3B1F0C2748004084F4 /* OWSAddToContactViewController.m in Sources */, 34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */, @@ -2940,6 +2946,7 @@ FCC81A981A44558300DFEC7D /* UIDevice+TSHardwareVersion.m in Sources */, 76EB054018170B33006006FC /* AppDelegate.m in Sources */, 34D1F0831F8678AA0066283D /* ConversationInputTextView.m in Sources */, + 347E0B7A2003CD7500BC2F76 /* OWSBackupExportViewController.m in Sources */, 34D1F0B11F867BFC0066283D /* OWSUnreadIndicatorCell.m in Sources */, 34B3F89C1E8DF3270035BE1A /* BlockListViewController.m in Sources */, 34BECE2B1F74C12700D7438D /* DebugUIStress.m in Sources */, diff --git a/Signal/src/ViewControllers/AppSettingsViewController.m b/Signal/src/ViewControllers/AppSettingsViewController.m index 1a93ab87aa..13da6516a6 100644 --- a/Signal/src/ViewControllers/AppSettingsViewController.m +++ b/Signal/src/ViewControllers/AppSettingsViewController.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "AppSettingsViewController.h" @@ -89,6 +89,10 @@ self.title = NSLocalizedString(@"SETTINGS_NAV_BAR_TITLE", @"Title for settings activity"); [self updateTableContents]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self showDebugUI]; + }); } - (void)viewWillAppear:(BOOL)animated diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 7bd7c77229..8e06f71a9a 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "OWSMessageCell.h" @@ -8,6 +8,8 @@ #import "ConversationViewItem.h" #import "NSAttributedString+OWS.h" #import "OWSAudioMessageView.h" +#import "OWSBackup.h" +#import "OWSBackupImportViewController.h" #import "OWSExpirationTimerView.h" #import "OWSGenericAttachmentView.h" #import "Signal-Swift.h" @@ -1314,7 +1316,16 @@ NS_ASSUME_NONNULL_BEGIN [self.delegate didTapVideoViewItem:self.viewItem attachmentStream:self.attachmentStream]; return; case OWSMessageCellType_GenericAttachment: +#ifdef DEBUG + if ([self.attachmentStream.filePath.lastPathComponent hasSuffix:OWSBackup_FileExtension]) { + [self showBackupImportConfirmAlert:self.attachmentStream.filePath]; + } else { + [AttachmentSharing showShareUIForAttachment:self.attachmentStream]; + } +#else [AttachmentSharing showShareUIForAttachment:self.attachmentStream]; +#endif + break; case OWSMessageCellType_DownloadingAttachment: { OWSAssert(self.attachmentPointer); @@ -1326,6 +1337,40 @@ NS_ASSUME_NONNULL_BEGIN } } +- (void)showBackupImportConfirmAlert:(NSString *)backupZipPath +{ + OWSAssert(backupZipPath.length > 0); + + UIAlertController *controller = + [UIAlertController alertControllerWithTitle:NSLocalizedString(@"BACKUP_IMPORT_CONFIRM_ALERT_TITLE", + @"Title for alert confirming backup import.") + message:NSLocalizedString(@"BACKUP_IMPORT_CONFIRM_ALERT_MESSAGE", + @"Message for alert confirming backup import.") + preferredStyle:UIAlertControllerStyleAlert]; + + [controller addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"BACKUP_IMPORT_CONFIRM_ALERT_BUTTON", + @"Label for button confirming backup import.") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *_Nonnull action) { + [self showBackupImportUI:backupZipPath]; + }]]; + [controller addAction:OWSAlerts.cancelAction]; + + UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController]; + [fromViewController presentViewController:controller animated:YES completion:nil]; +} + +- (void)showBackupImportUI:(NSString *)backupZipPath +{ + OWSAssert(backupZipPath.length > 0); + + OWSBackupImportViewController *backupViewController = [OWSBackupImportViewController new]; + // TODO: Add support for restoring password-protected backups. + [backupViewController importBackup:backupZipPath password:nil]; + UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController]; + [fromViewController presentViewController:backupViewController animated:YES completion:nil]; +} + - (void)handleTextLongPressGesture:(UILongPressGestureRecognizer *)sender { OWSAssert(self.delegate); diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m index aff68bf7f3..30cc540e08 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m @@ -4,7 +4,7 @@ #import "DebugUIMisc.h" #import "OWSBackup.h" -#import "OWSBackupViewController.h" +#import "OWSBackupExportViewController.h" #import "OWSCountryMetadata.h" #import "OWSTableViewController.h" #import "RegistrationViewController.h" @@ -98,7 +98,8 @@ NS_ASSUME_NONNULL_BEGIN [items addObject:[OWSTableItem subPageItemWithText:@"Export Backup w/ Password" actionBlock:^(UIViewController *viewController) { - OWSBackupViewController *backupViewController = [OWSBackupViewController new]; + OWSBackupExportViewController *backupViewController = + [OWSBackupExportViewController new]; [backupViewController exportBackup:thread skipPassword:NO]; [viewController.navigationController pushViewController:backupViewController animated:YES]; @@ -106,7 +107,8 @@ NS_ASSUME_NONNULL_BEGIN [items addObject:[OWSTableItem subPageItemWithText:@"Export Backup w/o Password" actionBlock:^(UIViewController *viewController) { - OWSBackupViewController *backupViewController = [OWSBackupViewController new]; + OWSBackupExportViewController *backupViewController = + [OWSBackupExportViewController new]; [backupViewController exportBackup:thread skipPassword:YES]; [viewController.navigationController pushViewController:backupViewController animated:YES]; diff --git a/Signal/src/ViewControllers/HomeViewController.m b/Signal/src/ViewControllers/HomeViewController.m index 9ae85caab6..bc6f6c8e7d 100644 --- a/Signal/src/ViewControllers/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeViewController.m @@ -286,6 +286,10 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; } [self updateBarButtonItems]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self settingsButtonPressed:nil]; + }); } - (void)updateBarButtonItems diff --git a/Signal/src/ViewControllers/OWSBackupViewController.h b/Signal/src/ViewControllers/OWSBackupExportViewController.h similarity index 86% rename from Signal/src/ViewControllers/OWSBackupViewController.h rename to Signal/src/ViewControllers/OWSBackupExportViewController.h index f4467ac81a..ef5586dcd2 100644 --- a/Signal/src/ViewControllers/OWSBackupViewController.h +++ b/Signal/src/ViewControllers/OWSBackupExportViewController.h @@ -8,7 +8,7 @@ NS_ASSUME_NONNULL_BEGIN @class TSThread; -@interface OWSBackupViewController : OWSViewController +@interface OWSBackupExportViewController : OWSViewController // If currentThread is non-nil, we should offer to let users send the // backup in that thread. diff --git a/Signal/src/ViewControllers/OWSBackupViewController.m b/Signal/src/ViewControllers/OWSBackupExportViewController.m similarity index 98% rename from Signal/src/ViewControllers/OWSBackupViewController.m rename to Signal/src/ViewControllers/OWSBackupExportViewController.m index 2b3bb03c09..82d169e71d 100644 --- a/Signal/src/ViewControllers/OWSBackupViewController.m +++ b/Signal/src/ViewControllers/OWSBackupExportViewController.m @@ -2,7 +2,7 @@ // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // -#import "OWSBackupViewController.h" +#import "OWSBackupExportViewController.h" #import "OWSBackup.h" #import "OWSProgressView.h" #import "Signal-Swift.h" @@ -17,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN -@interface OWSBackupViewController () +@interface OWSBackupExportViewController () @property (nonatomic) OWSBackup *backup; @@ -27,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - -@implementation OWSBackupViewController +@implementation OWSBackupExportViewController - (void)loadView { @@ -299,7 +299,6 @@ NS_ASSUME_NONNULL_BEGIN [self updateUI]; } - - (void)backupProgressDidChange { DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); diff --git a/Signal/src/ViewControllers/OWSBackupImportViewController.h b/Signal/src/ViewControllers/OWSBackupImportViewController.h new file mode 100644 index 0000000000..b62502c8f0 --- /dev/null +++ b/Signal/src/ViewControllers/OWSBackupImportViewController.h @@ -0,0 +1,17 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class TSThread; + +@interface OWSBackupImportViewController : OWSViewController + +- (void)importBackup:(NSString *)backupZipPath password:(NSString *_Nullable)password; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/OWSBackupImportViewController.m b/Signal/src/ViewControllers/OWSBackupImportViewController.m new file mode 100644 index 0000000000..e58db16b34 --- /dev/null +++ b/Signal/src/ViewControllers/OWSBackupImportViewController.m @@ -0,0 +1,242 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSBackupImportViewController.h" +#import "OWSBackup.h" +#import "OWSProgressView.h" +#import "Signal-Swift.h" +#import "ThreadUtil.h" +#import +#import +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSBackupImportViewController () + +@property (nonatomic) OWSBackup *backup; + +@property (nonatomic, nullable) OWSProgressView *progressView; + +@end + +#pragma mark - + +@implementation OWSBackupImportViewController + +- (void)loadView +{ + [super loadView]; + + self.view.backgroundColor = [UIColor whiteColor]; + + self.navigationItem.title = NSLocalizedString(@"BACKUP_IMPORT_VIEW_TITLE", @"Title for the 'backup import' view."); + self.navigationItem.leftBarButtonItem = + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemStop + target:self + action:@selector(dismissWasPressed:)]; + + self.backup.delegate = self; + + [self updateUI]; +} + +- (void)importBackup:(NSString *)backupZipPath password:(NSString *_Nullable)password +{ + OWSAssertIsOnMainThread(); + OWSAssert(backupZipPath.length > 0); + + // We set ourselves as the delegate of the backup later, + // after we've loaded our view. + self.backup = [OWSBackup new]; + [self.backup importBackup:backupZipPath password:password]; +} + +- (void)updateUI +{ + for (UIView *subview in self.view.subviews) { + [subview removeFromSuperview]; + } + self.progressView = nil; + + switch (self.backup.backupState) { + case OWSBackupState_InProgress: + [self showInProgressUI]; + break; + case OWSBackupState_Cancelled: + [self showCancelledUI]; + break; + case OWSBackupState_Complete: + [self showCompleteUI]; + break; + case OWSBackupState_Failed: + [self showFailedUI]; + break; + } +} + +- (void)showInProgressUI +{ + self.progressView = [OWSProgressView new]; + self.progressView.color = [UIColor ows_materialBlueColor]; + self.progressView.progress = self.backup.backupProgress; + [self.progressView autoSetDimension:ALDimensionWidth toSize:300]; + [self.progressView autoSetDimension:ALDimensionHeight toSize:20]; + + UILabel *label = [UILabel new]; + label.text = NSLocalizedString( + @"BACKUP_IMPORT_IN_PROGRESS_MESSAGE", @"Message indicating that backup import is in progress."); + label.textColor = [UIColor blackColor]; + label.font = [UIFont ows_regularFontWithSize:18.f]; + label.textAlignment = NSTextAlignmentCenter; + label.numberOfLines = 0; + label.lineBreakMode = NSLineBreakByWordWrapping; + + UIView *container = [UIView verticalStackWithSubviews:@[ + label, + self.progressView, + ] + spacing:10]; + [self.view addSubview:container]; + [container autoVCenterInSuperview]; + [container autoPinWidthToSuperviewWithMargin:25.f]; +} + +- (void)showCancelledUI +{ + // Show nothing. +} + +- (void)showCompleteUI +{ + NSMutableArray *subviews = [NSMutableArray new]; + + { + NSString *message = NSLocalizedString( + @"BACKUP_IMPORT_COMPLETE_MESSAGE", @"Message indicating that backup import is complete."); + + UILabel *label = [UILabel new]; + label.text = message; + label.textColor = [UIColor blackColor]; + label.font = [UIFont ows_regularFontWithSize:18.f]; + label.textAlignment = NSTextAlignmentCenter; + label.numberOfLines = 0; + label.lineBreakMode = NSLineBreakByWordWrapping; + [subviews addObject:label]; + } + + if (self.backup.backupPassword) { + NSString *message = [NSString stringWithFormat:NSLocalizedString(@"BACKUP_IMPORT_PASSWORD_MESSAGE_FORMAT", + @"Format for message indicating that backup import " + @"is complete. Embeds: {{the backup password}}."), + self.backup.backupPassword]; + + UILabel *label = [UILabel new]; + label.text = message; + label.textColor = [UIColor blackColor]; + label.font = [UIFont ows_regularFontWithSize:14.f]; + label.textAlignment = NSTextAlignmentCenter; + label.numberOfLines = 0; + label.lineBreakMode = NSLineBreakByWordWrapping; + [subviews addObject:label]; + } + + [subviews addObject:[UIView new]]; + + if (self.backup.backupPassword) { + [subviews + addObject:[self makeButtonWithTitle:NSLocalizedString(@"BACKUP_IMPORT_COPY_PASSWORD_BUTTON", + @"Label for button that copies backup password to the pasteboard.") + selector:@selector(copyPassword)]]; + } + + [subviews addObject:[self makeButtonWithTitle:NSLocalizedString(@"BACKUP_IMPORT_SHARE_BACKUP_BUTTON", + @"Label for button that opens share UI for backup.") + selector:@selector(shareBackup)]]; + + if (self.backup.currentThread) { + [subviews + addObject:[self makeButtonWithTitle:NSLocalizedString(@"BACKUP_IMPORT_SEND_BACKUP_BUTTON", + @"Label for button that 'send backup' in the current conversation.") + selector:@selector(sendBackup)]]; + } + + // TODO: We should offer the option to save the backup to "Files", iCloud, Dropbox, etc. + + UIView *container = [UIView verticalStackWithSubviews:subviews spacing:10]; + [self.view addSubview:container]; + [container autoVCenterInSuperview]; + [container autoPinWidthToSuperviewWithMargin:25.f]; +} + +- (void)showFailedUI +{ + NSMutableArray *subviews = [NSMutableArray new]; + + { + NSString *message + = NSLocalizedString(@"BACKUP_IMPORT_FAILED_MESSAGE", @"Message indicating that backup import failed."); + + UILabel *label = [UILabel new]; + label.text = message; + label.textColor = [UIColor blackColor]; + label.font = [UIFont ows_regularFontWithSize:18.f]; + label.textAlignment = NSTextAlignmentCenter; + label.numberOfLines = 0; + label.lineBreakMode = NSLineBreakByWordWrapping; + [subviews addObject:label]; + } + + // TODO: We should offer the option to save the backup to "Files", iCloud, Dropbox, etc. + + UIView *container = [UIView verticalStackWithSubviews:subviews spacing:10]; + [self.view addSubview:container]; + [container autoVCenterInSuperview]; + [container autoPinWidthToSuperviewWithMargin:25.f]; +} + +- (UIView *)makeButtonWithTitle:(NSString *)title selector:(SEL)selector +{ + const CGFloat kButtonHeight = 40; + OWSFlatButton *button = [OWSFlatButton buttonWithTitle:title + font:[OWSFlatButton fontForHeight:kButtonHeight] + titleColor:[UIColor whiteColor] + backgroundColor:[UIColor ows_materialBlueColor] + target:self + selector:selector]; + [button autoSetDimension:ALDimensionWidth toSize:140]; + [button autoSetDimension:ALDimensionHeight toSize:kButtonHeight]; + return button; +} + +- (void)dismissWasPressed:(id)sender +{ + [self.backup cancel]; + + [self.navigationController popViewControllerAnimated:YES]; +} + +#pragma mark - OWSBackupDelegate + +- (void)backupStateDidChange +{ + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + + [self updateUI]; +} + +- (void)backupProgressDidChange +{ + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + + self.progressView.progress = self.backup.backupProgress; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/util/OWSBackup.h b/Signal/src/util/OWSBackup.h index 85d5bba943..ce462ecb36 100644 --- a/Signal/src/util/OWSBackup.h +++ b/Signal/src/util/OWSBackup.h @@ -4,6 +4,8 @@ NS_ASSUME_NONNULL_BEGIN +extern NSString *const OWSBackup_FileExtension; + @protocol OWSBackupDelegate - (void)backupStateDidChange; @@ -40,6 +42,8 @@ typedef NS_ENUM(NSUInteger, OWSBackupState) { - (void)exportBackup:(nullable TSThread *)currentThread skipPassword:(BOOL)skipPassword; +- (void)importBackup:(NSString *)backupZipPath password:(NSString *_Nullable)password; + - (void)cancel; @end diff --git a/Signal/src/util/OWSBackup.m b/Signal/src/util/OWSBackup.m index 376d5940a2..5eb984de26 100644 --- a/Signal/src/util/OWSBackup.m +++ b/Signal/src/util/OWSBackup.m @@ -8,11 +8,15 @@ #import "zlib.h" #import #import +#import #import #import NS_ASSUME_NONNULL_BEGIN +NSString *const OWSBackup_FileExtension = @".signalbackup"; +NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilename"; + @interface OWSStorage (OWSBackup) - (NSData *)databasePassword; @@ -64,7 +68,16 @@ NS_ASSUME_NONNULL_BEGIN - (void)cancel { - self.backupState = OWSBackupState_Cancelled; + if (!self.isCancelledOrFailed) { + self.backupState = OWSBackupState_Cancelled; + } +} + +- (void)complete +{ + if (!self.isCancelledOrFailed) { + self.backupState = OWSBackupState_Complete; + } } - (BOOL)isCancelledOrFailed @@ -72,9 +85,12 @@ NS_ASSUME_NONNULL_BEGIN return (self.backupState == OWSBackupState_Cancelled || self.backupState == OWSBackupState_Failed); } +#pragma mark - Export Backup + - (void)exportBackup:(nullable TSThread *)currentThread skipPassword:(BOOL)skipPassword { OWSAssertIsOnMainThread(); + OWSAssert(CurrentAppContext().isMainApp); self.currentThread = currentThread; self.backupState = OWSBackupState_InProgress; @@ -101,10 +117,7 @@ NS_ASSUME_NONNULL_BEGIN [self exportToFilesAndZip]; dispatch_async(dispatch_get_main_queue(), ^{ - if (!self.isCancelledOrFailed) { - self.backupState = OWSBackupState_Complete; - } - [self.delegate backupStateDidChange]; + [self complete]; }); }); } @@ -125,7 +138,7 @@ NS_ASSUME_NONNULL_BEGIN @"Should not include characters like slash (/ or \\) or colon (:)."), backupDateTime]; NSString *backupZipPath = - [rootDirPath stringByAppendingPathComponent:[backupName stringByAppendingString:@".signalbackup"]]; + [rootDirPath stringByAppendingPathComponent:[backupName stringByAppendingString:OWSBackup_FileExtension]]; self.backupDirPath = backupDirPath; self.backupZipPath = backupZipPath; DDLogInfo(@"%@ rootDirPath: %@", self.logTag, rootDirPath); @@ -140,29 +153,35 @@ NS_ASSUME_NONNULL_BEGIN return; } + OWSAES256Key *encryptionKey = [OWSAES256Key generateRandomKey]; + NSData *databasePassword = [TSStorageManager sharedManager].databasePassword; - if (![self writeData:databasePassword fileName:@"databasePassword" backupDirPath:backupDirPath]) { - [self fail]; - return; + // TODO: We don't want this to reside unencrypted on disk even temporarily. + // We need to encrypt this with a key that we hide in the keychain. + if (![self writeData:databasePassword + fileName:@"databasePassword" + backupDirPath:backupDirPath + encryptionKey:encryptionKey]) { + return [self fail]; } if (self.isCancelledOrFailed) { return; } if (![self writeUserDefaults:NSUserDefaults.standardUserDefaults fileName:@"standardUserDefaults" - backupDirPath:backupDirPath]) { - [self fail]; - return; + backupDirPath:backupDirPath + encryptionKey:encryptionKey]) { + return [self fail]; } if (self.isCancelledOrFailed) { return; } if (![self writeUserDefaults:NSUserDefaults.appUserDefaults fileName:@"appUserDefaults" - backupDirPath:backupDirPath]) { - [self fail]; - return; + backupDirPath:backupDirPath + encryptionKey:encryptionKey]) { + return [self fail]; } if (self.isCancelledOrFailed) { return; @@ -191,9 +210,8 @@ NS_ASSUME_NONNULL_BEGIN if (self.isCancelledOrFailed) { return; } - if (![self zipDirectory:backupDirPath dstFilePath:backupZipPath]) { - [self fail]; - return; + if (![self zipDirectory:backupDirPath dstFilePath:backupZipPath encryptionKey:encryptionKey]) { + return [self fail]; } [OWSFileSystem protectFolderAtPath:backupZipPath]; @@ -201,18 +219,26 @@ NS_ASSUME_NONNULL_BEGIN [OWSFileSystem deleteFileIfExists:self.backupDirPath]; } -- (BOOL)writeData:(NSData *)data fileName:(NSString *)fileName backupDirPath:(NSString *)backupDirPath +// TODO: We +- (BOOL)writeData:(NSData *)data + fileName:(NSString *)fileName + backupDirPath:(NSString *)backupDirPath + encryptionKey:(OWSAES256Key *)encryptionKey { OWSAssert(data); OWSAssert(fileName.length > 0); OWSAssert(backupDirPath.length > 0); + OWSAssert(encryptionKey); + + NSData *encryptedData = [Cryptography encryptAESGCMWithData:data key:encryptionKey]; + OWSAssert(encryptedData); NSString *filePath = [backupDirPath stringByAppendingPathComponent:fileName]; DDLogVerbose(@"%@ writeData: %@", self.logTag, filePath); NSError *error; - BOOL success = [data writeToFile:filePath options:NSDataWritingAtomic error:&error]; + BOOL success = [encryptedData writeToFile:filePath options:NSDataWritingAtomic error:&error]; if (!success || error) { OWSFail(@"%@ failed to write user defaults: %@", self.logTag, error); return NO; @@ -260,10 +286,12 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)writeUserDefaults:(NSUserDefaults *)userDefaults fileName:(NSString *)fileName backupDirPath:(NSString *)backupDirPath + encryptionKey:(OWSAES256Key *)encryptionKey { OWSAssert(userDefaults); OWSAssert(fileName.length > 0); OWSAssert(backupDirPath.length > 0); + OWSAssert(encryptionKey); DDLogVerbose(@"%@ writeUserDefaults: %@", self.logTag, fileName); @@ -278,38 +306,91 @@ NS_ASSUME_NONNULL_BEGIN return NO; } - return [self writeData:data fileName:fileName backupDirPath:backupDirPath]; + return [self writeData:data fileName:fileName backupDirPath:backupDirPath encryptionKey:encryptionKey]; } -- (BOOL)zipDirectory:(NSString *)srcDirPath dstFilePath:(NSString *)dstFilePath +- (BOOL)zipDirectory:(NSString *)srcDirPath + dstFilePath:(NSString *)dstFilePath + encryptionKey:(OWSAES256Key *)encryptionKey { OWSAssert(srcDirPath.length > 0); OWSAssert(dstFilePath.length > 0); + OWSAssert(encryptionKey); - BOOL success = [SSZipArchive createZipFileAtPath:dstFilePath - withContentsOfDirectory:srcDirPath - keepParentDirectory:NO - compressionLevel:Z_DEFAULT_COMPRESSION - password:self.backupPassword - AES:self.backupPassword != nil - progressHandler:^(NSUInteger entryNumber, NSUInteger total) { - DDLogVerbose(@"%@ Zip progress: %zd / %zd = %f", - self.logTag, - entryNumber, - total, - entryNumber / (CGFloat)total); + srcDirPath = [srcDirPath stringByStandardizingPath]; + OWSAssert(srcDirPath.length > 0); - CGFloat progress = entryNumber / (CGFloat)total; - self.backupProgress = progress; - [self.delegate backupProgressDidChange]; - }]; + // BOOL success = [SSZipArchive createZipFileAtPath:dstFilePath + // withContentsOfDirectory:srcDirPath + // keepParentDirectory:NO + // compressionLevel:Z_DEFAULT_COMPRESSION + // password:self.backupPassword + // AES:self.backupPassword != nil + // progressHandler:^(NSUInteger entryNumber, NSUInteger total) { + // DDLogVerbose(@"%@ Zip progress: %zd / %zd = %f", + // self.logTag, + // entryNumber, + // total, + // entryNumber / (CGFloat)total); + // + // CGFloat progress = entryNumber / (CGFloat)total; + // self.backupProgress = progress; + // [self.delegate backupProgressDidChange]; + // }]; + // if (!success) { + // OWSFail(@"%@ failed to write zip backup", self.logTag); + // return NO; + // } - if (!success) { - OWSFail(@"%@ failed to write zip backup", self.logTag); + NSError *error; + NSArray *_Nullable srcFilePaths = [OWSFileSystem allFilesInDirectoryRecursive:srcDirPath error:&error]; + if (!srcFilePaths || error) { + OWSFail(@"%@ failed to find files to zip: %@", self.logTag, error); + return NO; + } + + SSZipArchive *zipArchive = [[SSZipArchive alloc] initWithPath:dstFilePath]; + if (![zipArchive open]) { + OWSFail(@"%@ failed to open zip file.", self.logTag); + return NO; + } + for (NSString *srcFilePath in srcFilePaths) { + OWSAssert(srcFilePath.stringByStandardizingPath.length > 0); + OWSAssert([srcFilePath.stringByStandardizingPath hasPrefix:srcDirPath]); + NSString *relativePath = [srcFilePath.stringByStandardizingPath substringFromIndex:srcDirPath.length]; + NSString *separator = @"/"; + if ([relativePath hasPrefix:separator]) { + relativePath = [relativePath substringFromIndex:separator.length]; + } + OWSAssert(relativePath.length > 0); + BOOL success = [zipArchive writeFileAtPath:srcFilePath + withFileName:relativePath + compressionLevel:Z_DEFAULT_COMPRESSION + password:self.backupPassword + AES:self.backupPassword != nil]; + if (!success) { + OWSFail(@"%@ failed to write file to zip file.", self.logTag); + return NO; + } + } + // Write the encryption key directly into the zip so that it never + // resides in plaintext on disk. + BOOL success = [zipArchive writeData:encryptionKey.keyData + filename:OWSBackup_EncryptionKeyFilename + compressionLevel:Z_DEFAULT_COMPRESSION + password:self.backupPassword + AES:self.backupPassword != nil]; + if (!success) { + OWSFail(@"%@ failed to write file to zip file.", self.logTag); + return NO; + } + + + if (![zipArchive close]) { + OWSFail(@"%@ failed to close zip file.", 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); @@ -320,6 +401,129 @@ NS_ASSUME_NONNULL_BEGIN return YES; } +#pragma mark - Import Backup + +- (void)importBackup:(NSString *)srcZipPath password:(NSString *_Nullable)password +{ + OWSAssertIsOnMainThread(); + OWSAssert(srcZipPath.length > 0); + OWSAssert(CurrentAppContext().isMainApp); + + self.backupPassword = password; + + self.backupState = OWSBackupState_InProgress; + + if (password.length == 0) { + DDLogVerbose(@"%@ backup import without password", self.logTag); + } else { + DDLogVerbose(@"%@ backup import with password: %@", self.logTag, password); + } + + [self startExport]; +} + +- (void)startExport:(NSString *)srcZipPath +{ + OWSAssertIsOnMainThread(); + OWSAssert(srcZipPath.length > 0); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self unpackFiles:srcZipPath]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self complete]; + }); + }); +} + +- (void)unpackFiles:(NSString *)srcZipPath +{ + OWSAssert(srcZipPath.length > 0); + + NSString *documentDirectoryPath = OWSFileSystem.appDocumentDirectoryPath; + // Hide the "import" directory from exports, etc. by prefixing with a period. + NSString *rootDirName = [@"." stringByAppendingString:[NSUUID UUID].UUIDString]; + NSString *rootDirPath = [documentDirectoryPath stringByAppendingPathComponent:rootDirName]; + NSString *backupDirPath = [rootDirPath stringByAppendingPathComponent:@"Contents"]; + NSString *backupZipPath = [rootDirPath stringByAppendingPathComponent:srcZipPath.lastPathComponent]; + self.backupDirPath = backupDirPath; + self.backupZipPath = backupZipPath; + DDLogInfo(@"%@ rootDirPath: %@", self.logTag, rootDirPath); + DDLogInfo(@"%@ backupDirPath: %@", self.logTag, backupDirPath); + DDLogInfo(@"%@ backupZipPath: %@", self.logTag, backupZipPath); + + [OWSFileSystem ensureDirectoryExists:rootDirPath]; + [OWSFileSystem protectFolderAtPath:rootDirPath]; + [OWSFileSystem ensureDirectoryExists:backupDirPath]; + + NSError *error = nil; + BOOL success = [[NSFileManager defaultManager] copyItemAtPath:srcZipPath toPath:backupZipPath error:&error]; + if (!success || error) { + OWSFail(@"%@ failed to copy backup zip: %@, %@", self.logTag, srcZipPath, error); + return [self fail]; + } + + if (self.isCancelledOrFailed) { + return; + } + + //// NSData *databasePassword = [TSStorageManager sharedManager].databasePassword; + // + // if (![self writeData:databasePassword fileName:@"databasePassword" backupDirPath:backupDirPath]) { + // return [self fail]; + // } + // if (self.isCancelledOrFailed) { + // return; + // } + // if (![self writeUserDefaults:NSUserDefaults.standardUserDefaults + // fileName:@"standardUserDefaults" + // backupDirPath:backupDirPath]) { + // return [self fail]; + // } + // if (self.isCancelledOrFailed) { + // return; + // } + // if (![self writeUserDefaults:NSUserDefaults.appUserDefaults + // fileName:@"appUserDefaults" + // backupDirPath:backupDirPath]) { + // return [self fail]; + // } + // if (self.isCancelledOrFailed) { + // return; + // } + // // Use a read/write transaction to acquire a file lock on the database files. + // // + // // TODO: If we use multiple database files, lock them too. + // [TSStorageManager.sharedManager.newDatabaseConnection + // readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + // if (![self copyDirectory:OWSFileSystem.appDocumentDirectoryPath + // dstDirName:@"appDocumentDirectoryPath" + // backupDirPath:backupDirPath]) { + // [self fail]; + // return; + // } + // if (self.isCancelledOrFailed) { + // return; + // } + // if (![self copyDirectory:OWSFileSystem.appSharedDataDirectoryPath + // dstDirName:@"appSharedDataDirectoryPath" + // backupDirPath:backupDirPath]) { + // [self fail]; + // return; + // } + // }]; + // if (self.isCancelledOrFailed) { + // return; + // } + // if (![self zipDirectory:backupDirPath dstFilePath:backupZipPath]) { + // return [self fail]; + // } + // + // [OWSFileSystem protectFolderAtPath:backupZipPath]; + // + // [OWSFileSystem deleteFileIfExists:self.backupDirPath]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 414abd6887..adaa5fff25 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -145,16 +145,19 @@ /* action sheet button title to enable built in speaker during a call */ "AUDIO_ROUTE_BUILT_IN_SPEAKER" = "Speaker"; -/* Message indicating that backup export without password is complete. */ +/* Message indicating that backup export is complete. */ "BACKUP_EXPORT_COMPLETE_MESSAGE" = "Backup complete."; /* Label for button that copies backup password to the pasteboard. */ "BACKUP_EXPORT_COPY_PASSWORD_BUTTON" = "Copy Password"; +/* Message indicating that backup export failed. */ +"BACKUP_EXPORT_FAILED_MESSAGE" = "BACKUP_EXPORT_FAILED_MESSAGE"; + /* Message indicating that backup export is in progress. */ "BACKUP_EXPORT_IN_PROGRESS_MESSAGE" = "Exporting Backup..."; -/* Format for message indicating that backup export with password is complete. Embeds: {{the backup password}}. */ +/* Format for message indicating that backup export is complete. Embeds: {{the backup password}}. */ "BACKUP_EXPORT_PASSWORD_MESSAGE_FORMAT" = "Your backup password is: %@. Make sure to keep a copy of this password or you won't be able to restore from this backup."; /* Label for button that 'send backup' in the current conversation. */ @@ -175,6 +178,39 @@ /* Format for backup filenames. Embeds: {{the date and time of the backup}}. Should not include characters like slash (/ or \\) or colon (:). */ "BACKUP_FILENAME_FORMAT" = "Signal Backup %@"; +/* Message indicating that backup import is complete. */ +"BACKUP_IMPORT_COMPLETE_MESSAGE" = "BACKUP_IMPORT_COMPLETE_MESSAGE"; + +/* Label for button confirming backup import. */ +"BACKUP_IMPORT_CONFIRM_ALERT_BUTTON" = "Restore"; + +/* Message for alert confirming backup import. */ +"BACKUP_IMPORT_CONFIRM_ALERT_MESSAGE" = "To restore this backup you must restart the Signal app. Warning: you will lose all of your current Signal data."; + +/* Title for alert confirming backup import. */ +"BACKUP_IMPORT_CONFIRM_ALERT_TITLE" = "Restore Backup?"; + +/* Label for button that copies backup password to the pasteboard. */ +"BACKUP_IMPORT_COPY_PASSWORD_BUTTON" = "BACKUP_IMPORT_COPY_PASSWORD_BUTTON"; + +/* Message indicating that backup import failed. */ +"BACKUP_IMPORT_FAILED_MESSAGE" = "BACKUP_IMPORT_FAILED_MESSAGE"; + +/* Message indicating that backup import is in progress. */ +"BACKUP_IMPORT_IN_PROGRESS_MESSAGE" = "BACKUP_IMPORT_IN_PROGRESS_MESSAGE"; + +/* Format for message indicating that backup import is complete. Embeds: {{the backup password}}. */ +"BACKUP_IMPORT_PASSWORD_MESSAGE_FORMAT" = "BACKUP_IMPORT_PASSWORD_MESSAGE_FORMAT"; + +/* Label for button that 'send backup' in the current conversation. */ +"BACKUP_IMPORT_SEND_BACKUP_BUTTON" = "BACKUP_IMPORT_SEND_BACKUP_BUTTON"; + +/* Label for button that opens share UI for backup. */ +"BACKUP_IMPORT_SHARE_BACKUP_BUTTON" = "BACKUP_IMPORT_SHARE_BACKUP_BUTTON"; + +/* Title for the 'backup import' view. */ +"BACKUP_IMPORT_VIEW_TITLE" = "BACKUP_IMPORT_VIEW_TITLE"; + /* An explanation of the consequences of blocking another user. */ "BLOCK_BEHAVIOR_EXPLANATION" = "Blocked users will not be able to call you or send you messages."; diff --git a/SignalServiceKit/src/Util/OWSFileSystem.h b/SignalServiceKit/src/Util/OWSFileSystem.h index 898266fa2c..54360d7c64 100644 --- a/SignalServiceKit/src/Util/OWSFileSystem.h +++ b/SignalServiceKit/src/Util/OWSFileSystem.h @@ -27,6 +27,8 @@ NS_ASSUME_NONNULL_BEGIN + (void)deleteFileIfExists:(NSString *)filePath; ++ (NSArray *_Nullable)allFilesInDirectoryRecursive:(NSString *)dirPath error:(NSError **)error; + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Util/OWSFileSystem.m b/SignalServiceKit/src/Util/OWSFileSystem.m index 11f4b2a1d3..0016283d61 100644 --- a/SignalServiceKit/src/Util/OWSFileSystem.m +++ b/SignalServiceKit/src/Util/OWSFileSystem.m @@ -131,6 +131,37 @@ NS_ASSUME_NONNULL_BEGIN } } ++ (NSArray *_Nullable)allFilesInDirectoryRecursive:(NSString *)dirPath error:(NSError **)error +{ + OWSAssert(dirPath.length > 0); + + *error = nil; + + NSArray *filenames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dirPath error:error]; + if (*error) { + return nil; + } + + NSMutableArray *filePaths = [NSMutableArray new]; + + for (NSString *filename in filenames) { + NSString *filePath = [dirPath stringByAppendingPathComponent:filename]; + + BOOL isDirectory; + [[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory]; + if (isDirectory) { + [filePaths addObjectsFromArray:[self allFilesInDirectoryRecursive:filePath error:error]]; + if (*error) { + return nil; + } + } else { + [filePaths addObject:filePath]; + } + } + + return filePaths; +} + @end NS_ASSUME_NONNULL_END From 16f731757908cfd2d5c6112f1cf197ef3ebce076 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 9 Jan 2018 10:12:14 -0500 Subject: [PATCH 10/15] Complete backup restore/import. --- Signal/src/AppDelegate.m | 7 +- .../AppSettingsViewController.m | 4 - .../ConversationView/Cells/OWSMessageCell.m | 4 +- .../src/ViewControllers/HomeViewController.m | 4 - .../OWSBackupImportViewController.m | 47 +- .../RegistrationViewController.m | 7 +- Signal/src/util/OWSBackup.h | 2 + Signal/src/util/OWSBackup.m | 602 ++++++++++++++---- .../translations/en.lproj/Localizable.strings | 21 +- SignalServiceKit/src/Storage/OWSStorage.m | 40 +- SignalServiceKit/src/Util/OWSFileSystem.h | 3 +- SignalServiceKit/src/Util/OWSFileSystem.m | 7 +- 12 files changed, 552 insertions(+), 196 deletions(-) diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 5bbd8ebf14..313887e48b 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "AppDelegate.h" @@ -9,6 +9,7 @@ #import "DebugLogger.h" #import "MainAppContext.h" #import "NotificationsManager.h" +#import "OWSBackup.h" #import "OWSNavigationController.h" #import "Pastelog.h" #import "PushManager.h" @@ -129,6 +130,10 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; SetRandFunctionSeed(); + // If a backup restore is in progress, try to complete it. + // Otherwise, cleanup backup state. + [OWSBackup applicationDidFinishLaunching]; + // XXX - careful when moving this. It must happen before we initialize TSStorageManager. [self verifyDBKeysAvailableBeforeBackgroundLaunch]; diff --git a/Signal/src/ViewControllers/AppSettingsViewController.m b/Signal/src/ViewControllers/AppSettingsViewController.m index 13da6516a6..8774344dab 100644 --- a/Signal/src/ViewControllers/AppSettingsViewController.m +++ b/Signal/src/ViewControllers/AppSettingsViewController.m @@ -89,10 +89,6 @@ self.title = NSLocalizedString(@"SETTINGS_NAV_BAR_TITLE", @"Title for settings activity"); [self updateTableContents]; - - dispatch_async(dispatch_get_main_queue(), ^{ - [self showDebugUI]; - }); } - (void)viewWillAppear:(BOOL)animated diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 8e06f71a9a..63d7dc82f6 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -1367,8 +1367,10 @@ NS_ASSUME_NONNULL_BEGIN OWSBackupImportViewController *backupViewController = [OWSBackupImportViewController new]; // TODO: Add support for restoring password-protected backups. [backupViewController importBackup:backupZipPath password:nil]; + UINavigationController *navigationController = + [[UINavigationController alloc] initWithRootViewController:backupViewController]; UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController]; - [fromViewController presentViewController:backupViewController animated:YES completion:nil]; + [fromViewController presentViewController:navigationController animated:YES completion:nil]; } - (void)handleTextLongPressGesture:(UILongPressGestureRecognizer *)sender diff --git a/Signal/src/ViewControllers/HomeViewController.m b/Signal/src/ViewControllers/HomeViewController.m index bc6f6c8e7d..9ae85caab6 100644 --- a/Signal/src/ViewControllers/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeViewController.m @@ -286,10 +286,6 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; } [self updateBarButtonItems]; - - dispatch_async(dispatch_get_main_queue(), ^{ - [self settingsButtonPressed:nil]; - }); } - (void)updateBarButtonItems diff --git a/Signal/src/ViewControllers/OWSBackupImportViewController.m b/Signal/src/ViewControllers/OWSBackupImportViewController.m index e58db16b34..a91c38c858 100644 --- a/Signal/src/ViewControllers/OWSBackupImportViewController.m +++ b/Signal/src/ViewControllers/OWSBackupImportViewController.m @@ -130,43 +130,11 @@ NS_ASSUME_NONNULL_BEGIN [subviews addObject:label]; } - if (self.backup.backupPassword) { - NSString *message = [NSString stringWithFormat:NSLocalizedString(@"BACKUP_IMPORT_PASSWORD_MESSAGE_FORMAT", - @"Format for message indicating that backup import " - @"is complete. Embeds: {{the backup password}}."), - self.backup.backupPassword]; - - UILabel *label = [UILabel new]; - label.text = message; - label.textColor = [UIColor blackColor]; - label.font = [UIFont ows_regularFontWithSize:14.f]; - label.textAlignment = NSTextAlignmentCenter; - label.numberOfLines = 0; - label.lineBreakMode = NSLineBreakByWordWrapping; - [subviews addObject:label]; - } - [subviews addObject:[UIView new]]; - if (self.backup.backupPassword) { - [subviews - addObject:[self makeButtonWithTitle:NSLocalizedString(@"BACKUP_IMPORT_COPY_PASSWORD_BUTTON", - @"Label for button that copies backup password to the pasteboard.") - selector:@selector(copyPassword)]]; - } - - [subviews addObject:[self makeButtonWithTitle:NSLocalizedString(@"BACKUP_IMPORT_SHARE_BACKUP_BUTTON", - @"Label for button that opens share UI for backup.") - selector:@selector(shareBackup)]]; - - if (self.backup.currentThread) { - [subviews - addObject:[self makeButtonWithTitle:NSLocalizedString(@"BACKUP_IMPORT_SEND_BACKUP_BUTTON", - @"Label for button that 'send backup' in the current conversation.") - selector:@selector(sendBackup)]]; - } - - // TODO: We should offer the option to save the backup to "Files", iCloud, Dropbox, etc. + [subviews addObject:[self makeButtonWithTitle:NSLocalizedString(@"BACKUP_IMPORT_RESTART_BUTTON", + @"Label for button that restarts app to complete restore.") + selector:@selector(restartApp)]]; UIView *container = [UIView verticalStackWithSubviews:subviews spacing:10]; [self.view addSubview:container]; @@ -218,7 +186,14 @@ NS_ASSUME_NONNULL_BEGIN { [self.backup cancel]; - [self.navigationController popViewControllerAnimated:YES]; + [self.navigationController dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)restartApp +{ + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + + [NSException raise:@"OWSBackup_RestartAppToCompleteBackupRestore" format:@"Killing app to complete backup restore"]; } #pragma mark - OWSBackupDelegate diff --git a/Signal/src/ViewControllers/RegistrationViewController.m b/Signal/src/ViewControllers/RegistrationViewController.m index 69247f72bd..69b2b310f9 100644 --- a/Signal/src/ViewControllers/RegistrationViewController.m +++ b/Signal/src/ViewControllers/RegistrationViewController.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "RegistrationViewController.h" @@ -439,8 +439,9 @@ NSString *const kKeychainKey_LastRegisteredPhoneNumber = @"kKeychainKey_LastRegi OWSCAssert(value.length > 0); NSError *error; - [SAMKeychain setPassword:value forService:kKeychainService_LastRegistered account:key error:&error]; - if (error) { + [SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly]; + BOOL success = [SAMKeychain setPassword:value forService:kKeychainService_LastRegistered account:key error:&error]; + if (!success || error) { DDLogError(@"%@ Error persisting 'last registered' value in keychain: %@", self.logTag, error); } } diff --git a/Signal/src/util/OWSBackup.h b/Signal/src/util/OWSBackup.h index ce462ecb36..d6e50a7beb 100644 --- a/Signal/src/util/OWSBackup.h +++ b/Signal/src/util/OWSBackup.h @@ -46,6 +46,8 @@ typedef NS_ENUM(NSUInteger, OWSBackupState) { - (void)cancel; ++ (void)applicationDidFinishLaunching; + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/util/OWSBackup.m b/Signal/src/util/OWSBackup.m index 5eb984de26..a08e3663b4 100644 --- a/Signal/src/util/OWSBackup.m +++ b/Signal/src/util/OWSBackup.m @@ -6,6 +6,7 @@ #import "NSUserDefaults+OWS.h" #import "Signal-Swift.h" #import "zlib.h" +#import #import #import #import @@ -14,18 +15,32 @@ NS_ASSUME_NONNULL_BEGIN +// Hide the "import" directories from exports, etc. by prefixing their name with a period. +NSString *const OWSBackup_DirNamePrefix = @".SignalBackup."; NSString *const OWSBackup_FileExtension = @".signalbackup"; NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilename"; +NSString *const OWSBackup_DatabasePasswordFilename = @".databasePassword"; +NSString *const OWSBackup_StandardUserDefaultsFilename = @".standardUserDefaults"; +NSString *const OWSBackup_AppUserDefaultsFilename = @".appUserDefaults"; +NSString *const OWSBackup_AppDocumentDirName = @"appDocumentDirectoryPath"; +NSString *const OWSBackup_AppSharedDataDirName = @"appSharedDataDirectoryPath"; + +NSString *const NSUserDefaults_QueuedBackupPath = @"NSUserDefaults_QueuedBackupPath"; + +NSString *const Keychain_ImportBackupService = @"OWSKeychainService"; +NSString *const Keychain_ImportBackupKey = @"ImportBackupKey"; @interface OWSStorage (OWSBackup) - (NSData *)databasePassword; ++ (void)storeDatabasePassword:(NSString *)password; + @end #pragma mark - -@interface OWSBackup () +@interface OWSBackup () @property (nonatomic) OWSBackupState backupState; @@ -38,6 +53,8 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen @property (nonatomic) NSString *backupDirPath; @property (nonatomic) NSString *backupZipPath; +@property (nonatomic) OWSAES256Key *encryptionKey; + @end #pragma mark - @@ -46,35 +63,62 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen - (void)dealloc { - OWSAssert(self.backupDirPath.length > 0); - DDLogInfo(@"%@ Cleaning up: %@", self.logTag, self.backupDirPath); [OWSFileSystem deleteFileIfExists:self.backupDirPath]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [OWSBackup cleanupBackupState]; + }); } - (void)setBackupState:(OWSBackupState)backupState { _backupState = backupState; - [self.delegate backupStateDidChange]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.delegate backupStateDidChange]; + }); +} + +- (void)setBackupProgress:(CGFloat)backupProgress +{ + _backupProgress = backupProgress; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self.delegate backupProgressDidChange]; + }); } - (void)fail { + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + if (!self.isCancelledOrFailed) { self.backupState = OWSBackupState_Failed; } + + dispatch_async(dispatch_get_main_queue(), ^{ + [OWSBackup cleanupBackupState]; + }); } - (void)cancel { + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + if (!self.isCancelledOrFailed) { self.backupState = OWSBackupState_Cancelled; } + + dispatch_async(dispatch_get_main_queue(), ^{ + [OWSBackup cleanupBackupState]; + }); } - (void)complete { + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + if (!self.isCancelledOrFailed) { self.backupState = OWSBackupState_Complete; } @@ -92,18 +136,20 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen OWSAssertIsOnMainThread(); OWSAssert(CurrentAppContext().isMainApp); + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + self.currentThread = currentThread; self.backupState = OWSBackupState_InProgress; if (skipPassword) { - DDLogVerbose(@"%@ backup export without password", self.logTag); + DDLogInfo(@"%@ backup export without password", self.logTag); } else { // TODO: Should the user pick a password? // If not, should probably generate something more user-friendly, // e.g. case-insensitive set of hexadecimal? NSString *backupPassword = [NSUUID UUID].UUIDString; self.backupPassword = backupPassword; - DDLogVerbose(@"%@ backup export with password: %@", self.logTag, backupPassword); + DDLogInfo(@"%@ backup export with password: %@", self.logTag, backupPassword); } [self startExport]; @@ -124,8 +170,11 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen - (void)exportToFilesAndZip { + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + NSString *temporaryDirectory = NSTemporaryDirectory(); - NSString *rootDirPath = [temporaryDirectory stringByAppendingPathComponent:[NSUUID UUID].UUIDString]; + NSString *rootDirName = [OWSBackup_DirNamePrefix stringByAppendingString:[NSUUID UUID].UUIDString]; + NSString *rootDirPath = [temporaryDirectory stringByAppendingPathComponent:rootDirName]; NSString *backupDirPath = [rootDirPath stringByAppendingPathComponent:@"Contents"]; NSDateFormatter *dateFormatter = [NSDateFormatter new]; @@ -154,13 +203,14 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen } OWSAES256Key *encryptionKey = [OWSAES256Key generateRandomKey]; + self.encryptionKey = encryptionKey; NSData *databasePassword = [TSStorageManager sharedManager].databasePassword; // TODO: We don't want this to reside unencrypted on disk even temporarily. // We need to encrypt this with a key that we hide in the keychain. if (![self writeData:databasePassword - fileName:@"databasePassword" + fileName:OWSBackup_DatabasePasswordFilename backupDirPath:backupDirPath encryptionKey:encryptionKey]) { return [self fail]; @@ -169,7 +219,7 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen return; } if (![self writeUserDefaults:NSUserDefaults.standardUserDefaults - fileName:@"standardUserDefaults" + fileName:OWSBackup_StandardUserDefaultsFilename backupDirPath:backupDirPath encryptionKey:encryptionKey]) { return [self fail]; @@ -178,7 +228,7 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen return; } if (![self writeUserDefaults:NSUserDefaults.appUserDefaults - fileName:@"appUserDefaults" + fileName:OWSBackup_AppUserDefaultsFilename backupDirPath:backupDirPath encryptionKey:encryptionKey]) { return [self fail]; @@ -192,7 +242,7 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen [TSStorageManager.sharedManager.newDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { if (![self copyDirectory:OWSFileSystem.appDocumentDirectoryPath - dstDirName:@"appDocumentDirectoryPath" + dstDirName:OWSBackup_AppDocumentDirName backupDirPath:backupDirPath]) { [self fail]; return; @@ -201,7 +251,7 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen return; } if (![self copyDirectory:OWSFileSystem.appSharedDataDirectoryPath - dstDirName:@"appSharedDataDirectoryPath" + dstDirName:OWSBackup_AppSharedDataDirName backupDirPath:backupDirPath]) { [self fail]; return; @@ -219,7 +269,6 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen [OWSFileSystem deleteFileIfExists:self.backupDirPath]; } -// TODO: We - (BOOL)writeData:(NSData *)data fileName:(NSString *)fileName backupDirPath:(NSString *)backupDirPath @@ -230,12 +279,15 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen OWSAssert(backupDirPath.length > 0); OWSAssert(encryptionKey); - NSData *encryptedData = [Cryptography encryptAESGCMWithData:data key:encryptionKey]; - OWSAssert(encryptedData); + NSData *_Nullable encryptedData = [Cryptography encryptAESGCMWithData:data key:encryptionKey]; + if (!encryptedData) { + OWSFail(@"%@ failed to encrypt data: %@", self.logTag, fileName); + return NO; + } NSString *filePath = [backupDirPath stringByAppendingPathComponent:fileName]; - DDLogVerbose(@"%@ writeData: %@", self.logTag, filePath); + DDLogInfo(@"%@ writeData: %@", self.logTag, filePath); NSError *error; BOOL success = [encryptedData writeToFile:filePath options:NSDataWritingAtomic error:&error]; @@ -254,7 +306,7 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen NSString *dstDirPath = [backupDirPath stringByAppendingPathComponent:dstDirName]; - DDLogVerbose(@"%@ copyDirectory: %@ -> %@", self.logTag, srcDirPath, dstDirPath); + DDLogInfo(@"%@ copyDirectory: %@ -> %@", self.logTag, srcDirPath, dstDirPath); // We "manually" copy the "root" items in the src directory. // Can't just use [NSFileManager copyItemAtPath:...] because the shared data container @@ -270,7 +322,7 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen NSString *srcFilePath = [srcDirPath stringByAppendingPathComponent:fileName]; NSString *dstFilePath = [dstDirPath stringByAppendingPathComponent:fileName]; if ([fileName hasPrefix:@"."]) { - DDLogVerbose(@"%@ ignoring: %@", self.logTag, srcFilePath); + DDLogInfo(@"%@ ignoring: %@", self.logTag, srcFilePath); continue; } BOOL success = [[NSFileManager defaultManager] copyItemAtPath:srcFilePath toPath:dstFilePath error:&error]; @@ -293,7 +345,7 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen OWSAssert(backupDirPath.length > 0); OWSAssert(encryptionKey); - DDLogVerbose(@"%@ writeUserDefaults: %@", self.logTag, fileName); + DDLogInfo(@"%@ writeUserDefaults: %@", self.logTag, fileName); NSDictionary *dictionary = userDefaults.dictionaryRepresentation; if (!dictionary) { @@ -317,31 +369,11 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen OWSAssert(dstFilePath.length > 0); OWSAssert(encryptionKey); + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + srcDirPath = [srcDirPath stringByStandardizingPath]; OWSAssert(srcDirPath.length > 0); - // BOOL success = [SSZipArchive createZipFileAtPath:dstFilePath - // withContentsOfDirectory:srcDirPath - // keepParentDirectory:NO - // compressionLevel:Z_DEFAULT_COMPRESSION - // password:self.backupPassword - // AES:self.backupPassword != nil - // progressHandler:^(NSUInteger entryNumber, NSUInteger total) { - // DDLogVerbose(@"%@ Zip progress: %zd / %zd = %f", - // self.logTag, - // entryNumber, - // total, - // entryNumber / (CGFloat)total); - // - // CGFloat progress = entryNumber / (CGFloat)total; - // self.backupProgress = progress; - // [self.delegate backupProgressDidChange]; - // }]; - // if (!success) { - // OWSFail(@"%@ failed to write zip backup", self.logTag); - // return NO; - // } - NSError *error; NSArray *_Nullable srcFilePaths = [OWSFileSystem allFilesInDirectoryRecursive:srcDirPath error:&error]; if (!srcFilePaths || error) { @@ -349,20 +381,15 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen return NO; } + // Don't use the SSZipArchive convenience methods so that we can add the + // encryption key directly as data. SSZipArchive *zipArchive = [[SSZipArchive alloc] initWithPath:dstFilePath]; if (![zipArchive open]) { OWSFail(@"%@ failed to open zip file.", self.logTag); return NO; } for (NSString *srcFilePath in srcFilePaths) { - OWSAssert(srcFilePath.stringByStandardizingPath.length > 0); - OWSAssert([srcFilePath.stringByStandardizingPath hasPrefix:srcDirPath]); - NSString *relativePath = [srcFilePath.stringByStandardizingPath substringFromIndex:srcDirPath.length]; - NSString *separator = @"/"; - if ([relativePath hasPrefix:separator]) { - relativePath = [relativePath substringFromIndex:separator.length]; - } - OWSAssert(relativePath.length > 0); + NSString *relativePath = [self relativePathforPath:srcFilePath basePath:srcDirPath]; BOOL success = [zipArchive writeFileAtPath:srcFilePath withFileName:relativePath compressionLevel:Z_DEFAULT_COMPRESSION @@ -396,12 +423,12 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen OWSFail(@"%@ failed to get zip file size: %@", self.logTag, error); return NO; } - DDLogVerbose(@"%@ Zip file size: %@", self.logTag, fileSize); + DDLogInfo(@"%@ Zip file size: %@", self.logTag, fileSize); return YES; } -#pragma mark - Import Backup +#pragma mark - Import Backup, Part 1 - (void)importBackup:(NSString *)srcZipPath password:(NSString *_Nullable)password { @@ -409,26 +436,28 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen OWSAssert(srcZipPath.length > 0); OWSAssert(CurrentAppContext().isMainApp); + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + self.backupPassword = password; self.backupState = OWSBackupState_InProgress; if (password.length == 0) { - DDLogVerbose(@"%@ backup import without password", self.logTag); + DDLogInfo(@"%@ backup import without password", self.logTag); } else { - DDLogVerbose(@"%@ backup import with password: %@", self.logTag, password); + DDLogInfo(@"%@ backup import with password: %@", self.logTag, password); } - [self startExport]; + [self startImport:srcZipPath]; } -- (void)startExport:(NSString *)srcZipPath +- (void)startImport:(NSString *)srcZipPath { OWSAssertIsOnMainThread(); OWSAssert(srcZipPath.length > 0); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [self unpackFiles:srcZipPath]; + [self prepareForImport:srcZipPath]; dispatch_async(dispatch_get_main_queue(), ^{ [self complete]; @@ -436,13 +465,14 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen }); } -- (void)unpackFiles:(NSString *)srcZipPath +- (void)prepareForImport:(NSString *)srcZipPath { OWSAssert(srcZipPath.length > 0); + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + NSString *documentDirectoryPath = OWSFileSystem.appDocumentDirectoryPath; - // Hide the "import" directory from exports, etc. by prefixing with a period. - NSString *rootDirName = [@"." stringByAppendingString:[NSUUID UUID].UUIDString]; + NSString *rootDirName = [OWSBackup_DirNamePrefix stringByAppendingString:[NSUUID UUID].UUIDString]; NSString *rootDirPath = [documentDirectoryPath stringByAppendingPathComponent:rootDirName]; NSString *backupDirPath = [rootDirPath stringByAppendingPathComponent:@"Contents"]; NSString *backupZipPath = [rootDirPath stringByAppendingPathComponent:srcZipPath.lastPathComponent]; @@ -466,62 +496,408 @@ NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilen if (self.isCancelledOrFailed) { return; } + if (![self unzipFilePath]) { + return [self fail]; + } + if (self.isCancelledOrFailed) { + return; + } + if (![self extractEncryptionKey]) { + return [self fail]; + } + if (self.isCancelledOrFailed) { + return; + } + if (![self isValidBackup]) { + return [self fail]; + } + if (self.isCancelledOrFailed) { + return; + } + if (![self enqueueBackupRestore]) { + return [self fail]; + } +} - //// NSData *databasePassword = [TSStorageManager sharedManager].databasePassword; +- (BOOL)extractEncryptionKey +{ + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + + NSString *encryptionKeyFilePath = + [self.backupDirPath stringByAppendingPathComponent:OWSBackup_EncryptionKeyFilename]; + if (![[NSFileManager defaultManager] fileExistsAtPath:encryptionKeyFilePath]) { + return NO; + } + NSData *_Nullable encryptionKeyData = [NSData dataWithContentsOfFile:encryptionKeyFilePath]; + if (!encryptionKeyData) { + return NO; + } + OWSAES256Key *encryptionKey = [OWSAES256Key keyWithData:encryptionKeyData]; + if (!encryptionKey) { + return NO; + } + self.encryptionKey = encryptionKey; + + NSError *error = nil; + BOOL success = [[NSFileManager defaultManager] removeItemAtPath:encryptionKeyFilePath error:&error]; + if (!success || error) { + OWSFail(@"%@ could not delete encryption key file: %@", self.logTag, error); + return NO; + } + return YES; +} + +- (BOOL)unzipFilePath +{ + OWSAssert(self.backupZipPath.length > 0); + OWSAssert(self.backupDirPath.length > 0); + + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + + // Don't use the SSZipArchive convenience methods so that we can add the + // encryption key directly as data. + + // TODO: Should we use preserveAttributes? + NSError *error = nil; + BOOL success = [SSZipArchive unzipFileAtPath:self.backupZipPath + toDestination:self.backupDirPath + preserveAttributes:YES + overwrite:YES + nestedZipLevel:0 + password:self.backupPassword + error:&error + delegate:self + progressHandler:^(NSString *entry, unz_file_info zipInfo, long entryNumber, long total) { + DDLogInfo(@"%@ progressHandler: %ld %ld", self.logTag, entryNumber, total); + + CGFloat progress = entryNumber / (CGFloat)total; + self.backupProgress = progress; + } + completionHandler:^(NSString *path, BOOL succeeded, NSError *_Nullable completionError) { + DDLogInfo(@"%@ completionHandler: %d %@", self.logTag, succeeded, completionError); + }]; + if (!success || error) { + OWSFail(@"%@ failed to unzip file: %@.", self.logTag, error); + return NO; + } + + return YES; +} + +- (BOOL)isValidBackup +{ + NSString *databasePasswordFilePath = + [self.backupDirPath stringByAppendingPathComponent:OWSBackup_DatabasePasswordFilename]; + if (![[NSFileManager defaultManager] fileExistsAtPath:databasePasswordFilePath]) { + return NO; + } + NSString *standardUserDefaultsFilePath = + [self.backupDirPath stringByAppendingPathComponent:OWSBackup_StandardUserDefaultsFilename]; + if (![[NSFileManager defaultManager] fileExistsAtPath:standardUserDefaultsFilePath]) { + return NO; + } + NSString *appUserDefaultsFilePath = + [self.backupDirPath stringByAppendingPathComponent:OWSBackup_AppUserDefaultsFilename]; + if (![[NSFileManager defaultManager] fileExistsAtPath:appUserDefaultsFilePath]) { + return NO; + } + // TODO: Verify that the primary database exists. + + return YES; +} + +- (BOOL)enqueueBackupRestore +{ + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + + NSError *error = nil; + [SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly]; + BOOL success = [SAMKeychain setPasswordData:self.encryptionKey.keyData + forService:Keychain_ImportBackupService + account:Keychain_ImportBackupKey + error:&error]; + if (!success || error) { + OWSFail(@"%@ Could not store encryption key for import backup: %@", self.logTag, error); + return NO; + } + + NSString *documentDirectoryPath = OWSFileSystem.appDocumentDirectoryPath; + NSString *relativePath = [self relativePathforPath:self.backupDirPath basePath:documentDirectoryPath]; + [[NSUserDefaults appUserDefaults] setObject:relativePath forKey:NSUserDefaults_QueuedBackupPath]; + [[NSUserDefaults appUserDefaults] synchronize]; + + return YES; +} + +#pragma mark - Import Backup, Part 2 + +- (void)completeImportBackupIfPossible +{ + OWSAssertIsOnMainThread(); + OWSAssert(CurrentAppContext().isMainApp); + + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + + NSString *_Nullable queuedBackupRelativePath = + [[NSUserDefaults appUserDefaults] stringForKey:NSUserDefaults_QueuedBackupPath]; + if (queuedBackupRelativePath.length == 0) { + return; + } + NSString *documentDirectoryPath = OWSFileSystem.appDocumentDirectoryPath; + NSString *_Nullable queuedBackupPath = + [self joinRelativePath:queuedBackupRelativePath basePath:documentDirectoryPath]; + if (![[NSFileManager defaultManager] fileExistsAtPath:queuedBackupPath]) { + OWSFail(@"%@ Missing import backup directory: %@.", self.logTag, queuedBackupPath); + return; + } + self.backupDirPath = queuedBackupPath; + self.backupState = OWSBackupState_InProgress; + DDLogInfo(@"%@ queuedBackupPath: %@", self.logTag, queuedBackupPath); + + [SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly]; + NSError *error; + NSData *_Nullable encryptionKeyData = + [SAMKeychain passwordDataForService:Keychain_ImportBackupService account:Keychain_ImportBackupKey error:&error]; + if (!encryptionKeyData || error) { + OWSFail(@"%@ Could not retrieve encryption key for import backup: %@", self.logTag, error); + return; + } + self.encryptionKey = [OWSAES256Key keyWithData:encryptionKeyData]; + + NSData *_Nullable databasePassword = [self readDataFromFileName:OWSBackup_DatabasePasswordFilename]; + if (!databasePassword) { + OWSFail(@"%@ Could not retrieve database password.", self.logTag); + return; + } + + // We can't restore a backup atomically, so we: // - // if (![self writeData:databasePassword fileName:@"databasePassword" backupDirPath:backupDirPath]) { - // return [self fail]; - // } - // if (self.isCancelledOrFailed) { - // return; - // } - // if (![self writeUserDefaults:NSUserDefaults.standardUserDefaults - // fileName:@"standardUserDefaults" - // backupDirPath:backupDirPath]) { - // return [self fail]; - // } - // if (self.isCancelledOrFailed) { - // return; - // } - // if (![self writeUserDefaults:NSUserDefaults.appUserDefaults - // fileName:@"appUserDefaults" - // backupDirPath:backupDirPath]) { - // return [self fail]; - // } - // if (self.isCancelledOrFailed) { - // return; - // } - // // Use a read/write transaction to acquire a file lock on the database files. - // // - // // TODO: If we use multiple database files, lock them too. - // [TSStorageManager.sharedManager.newDatabaseConnection - // readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { - // if (![self copyDirectory:OWSFileSystem.appDocumentDirectoryPath - // dstDirName:@"appDocumentDirectoryPath" - // backupDirPath:backupDirPath]) { - // [self fail]; - // return; - // } - // if (self.isCancelledOrFailed) { - // return; - // } - // if (![self copyDirectory:OWSFileSystem.appSharedDataDirectoryPath - // dstDirName:@"appSharedDataDirectoryPath" - // backupDirPath:backupDirPath]) { - // [self fail]; - // return; - // } - // }]; - // if (self.isCancelledOrFailed) { - // return; - // } - // if (![self zipDirectory:backupDirPath dstFilePath:backupZipPath]) { - // return [self fail]; - // } - // - // [OWSFileSystem protectFolderAtPath:backupZipPath]; - // - // [OWSFileSystem deleteFileIfExists:self.backupDirPath]; + // * Ensure the restore consists only of tiny writes, and file moves. + // * Write the database password last. + if (![self loadUserDefaults:NSUserDefaults.standardUserDefaults fileName:OWSBackup_StandardUserDefaultsFilename]) { + return; + } + if (![self loadUserDefaults:NSUserDefaults.appUserDefaults fileName:OWSBackup_AppUserDefaultsFilename]) { + return; + } + + if (![self restoreDirectoryContents:OWSFileSystem.appDocumentDirectoryPath + srcDirName:OWSBackup_AppDocumentDirName]) { + return; + } + if (![self restoreDirectoryContents:OWSFileSystem.appSharedDataDirectoryPath + srcDirName:OWSBackup_AppSharedDataDirName]) { + return; + } + + // TODO: Possibly verify database file location? + + [OWSStorage storeDatabasePassword:[[NSString alloc] initWithData:databasePassword encoding:NSUTF8StringEncoding]]; +} + +- (nullable NSData *)readDataFromFileName:(NSString *)fileName +{ + OWSAssert(fileName.length > 0); + OWSAssert(self.backupDirPath.length > 0); + OWSAssert(self.encryptionKey); + + NSString *filePath = [self.backupDirPath stringByAppendingPathComponent:fileName]; + + DDLogInfo(@"%@ readDataFromFileName: %@", self.logTag, filePath); + + NSData *_Nullable encryptedData = [NSData dataWithContentsOfFile:filePath]; + if (!encryptedData) { + OWSFail(@"%@ failed to read encrypted data: %@", self.logTag, fileName); + return nil; + } + + NSData *_Nullable data = [Cryptography decryptAESGCMWithData:encryptedData key:self.encryptionKey]; + if (!data) { + OWSFail(@"%@ failed to decrypt data: %@", self.logTag, fileName); + return nil; + } + + return data; +} + +- (BOOL)loadUserDefaults:(NSUserDefaults *)userDefaults fileName:(NSString *)fileName +{ + OWSAssert(userDefaults); + OWSAssert(fileName.length > 0); + OWSAssert(self.backupDirPath.length > 0); + OWSAssert(self.encryptionKey); + + DDLogInfo(@"%@ loadUserDefaults: %@", self.logTag, fileName); + + NSData *_Nullable data = [self readDataFromFileName:fileName]; + if (!data) { + OWSFail(@"%@ Could not retrieve user defaults: %@.", self.logTag, fileName); + return NO; + } + + NSError *error; + NSDictionary *_Nullable dictionary = + [NSKeyedUnarchiver unarchiveTopLevelObjectWithData:data error:&error]; + if (!dictionary || error) { + OWSFail(@"%@ Could not unarchive user defaults: %@", self.logTag, error); + return NO; + } + if (![dictionary isKindOfClass:[NSDictionary class]]) { + OWSFail(@"%@ Unexpected archived user defaults: %@", self.logTag, error); + return NO; + } + + // TODO: this doesn't yet remove any keys, so you end up with the "union". + for (NSString *key in dictionary) { + id value = dictionary[key]; + OWSAssert(value); + [userDefaults setObject:value forKey:key]; + } + + return YES; +} + +- (BOOL)restoreDirectoryContents:(NSString *)dstDirPath srcDirName:(NSString *)srcDirName +{ + OWSAssert(srcDirName.length > 0); + OWSAssert(dstDirPath.length > 0); + OWSAssert(self.backupDirPath.length > 0); + + NSString *srcDirPath = [self.backupDirPath stringByAppendingPathComponent:srcDirName]; + + DDLogInfo(@"%@ restoreDirectoryContents: %@ -> %@", self.logTag, srcDirPath, dstDirPath); + + if (![[NSFileManager defaultManager] fileExistsAtPath:srcDirPath]) { + DDLogInfo(@"%@ Skipping restore directory: %@.", self.logTag, srcDirPath); + return YES; + } + + NSError *error = nil; + NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:srcDirPath error:&error]; + if (error) { + OWSFail(@"%@ failed to list directory: %@, %@", self.logTag, srcDirPath, error); + return NO; + } + for (NSString *fileName in fileNames) { + NSString *srcFilePath = [srcDirPath stringByAppendingPathComponent:fileName]; + NSString *dstFilePath = [dstDirPath stringByAppendingPathComponent:fileName]; + + if ([[NSFileManager defaultManager] fileExistsAtPath:dstFilePath]) { + // To replace an existing file or directory, rename the existing item + // by adding a date/time suffix. + NSDateFormatter *dateFormatter = [NSDateFormatter new]; + [dateFormatter setLocale:[NSLocale currentLocale]]; + [dateFormatter setDateFormat:@".yyyy.MM.dd hh.mm.ss"]; + NSString *replacementDateTime = [dateFormatter stringFromDate:[NSDate new]]; + + NSString *oldFilePath = [dstFilePath stringByAppendingString:replacementDateTime]; + BOOL success = [[NSFileManager defaultManager] moveItemAtPath:dstFilePath toPath:oldFilePath error:&error]; + if (!success || error) { + OWSFail(@"%@ failed to move directory item: %@, %@", self.logTag, dstFilePath, error); + return NO; + } + if (![OWSFileSystem protectFolderAtPath:oldFilePath]) { + OWSFail(@"%@ failed to protect old directory item: %@, %@", self.logTag, oldFilePath, error); + return NO; + } + } + + BOOL success = [[NSFileManager defaultManager] moveItemAtPath:srcFilePath toPath:dstFilePath error:&error]; + if (!success || error) { + OWSFail(@"%@ failed to move directory item: %@, %@", self.logTag, dstFilePath, error); + return NO; + } + if (![OWSFileSystem protectFolderAtPath:dstFilePath]) { + OWSFail(@"%@ failed to protect directory item: %@, %@", self.logTag, dstFilePath, error); + return NO; + } + } + + return YES; +} + +#pragma mark - Clean up + ++ (void)cleanupBackupState +{ + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + + [self cleanupBackupDirectoriesInDirectory:NSTemporaryDirectory()]; + [self cleanupBackupDirectoriesInDirectory:OWSFileSystem.appDocumentDirectoryPath]; + + [[NSUserDefaults appUserDefaults] removeObjectForKey:NSUserDefaults_QueuedBackupPath]; + [[NSUserDefaults appUserDefaults] synchronize]; +} + ++ (void)cleanupBackupDirectoriesInDirectory:(NSString *)dirPath +{ + OWSAssert(dirPath.length > 0); + + NSError *error; + NSArray *filenames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dirPath error:&error]; + if (error) { + OWSFail(@"%@ could not find files in directory: %@", self.logTag, error); + return; + } + + for (NSString *filename in filenames) { + if (![filename hasPrefix:OWSBackup_DirNamePrefix]) { + continue; + } + NSString *filePath = [dirPath stringByAppendingPathComponent:filename]; + BOOL success = [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; + if (!success || error) { + OWSFail(@"%@ could not clean up backup directory: %@", self.logTag, error); + return; + } + } +} + +#pragma mark - Utils + +- (NSString *)relativePathforPath:(NSString *)filePath basePath:(NSString *)basePath +{ + OWSAssert(filePath.stringByStandardizingPath.length > 0); + OWSAssert([filePath.stringByStandardizingPath hasPrefix:basePath]); + + NSString *relativePath = [filePath.stringByStandardizingPath substringFromIndex:basePath.length]; + NSString *separator = @"/"; + if ([relativePath hasPrefix:separator]) { + relativePath = [relativePath substringFromIndex:separator.length]; + } + OWSAssert(relativePath.length > 0); + return relativePath; +} + +- (NSString *)joinRelativePath:(NSString *)relativePath basePath:(NSString *)basePath +{ + OWSAssert(basePath.stringByStandardizingPath.length > 0); + OWSAssert(relativePath.length > 0); + + return [basePath stringByAppendingPathComponent:relativePath]; +} + +#pragma mark - App Launch + ++ (void)applicationDidFinishLaunching +{ + [[OWSBackup new] completeImportBackupIfPossible]; + + // Always clean up backup state on disk, but defer so as not to interface with + // app launch sequence. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [OWSBackup cleanupBackupState]; + }); +} + +#pragma mark - SSZipArchiveDelegate + +- (void)zipArchiveProgressEvent:(unsigned long long)loaded total:(unsigned long long)total +{ + DDLogInfo(@"%@ zipArchiveProgressEvent: %llu %llu", self.logTag, loaded, total); + + CGFloat progress = loaded / (CGFloat)total; + self.backupProgress = progress; } @end diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index adaa5fff25..a770245370 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -179,7 +179,7 @@ "BACKUP_FILENAME_FORMAT" = "Signal Backup %@"; /* Message indicating that backup import is complete. */ -"BACKUP_IMPORT_COMPLETE_MESSAGE" = "BACKUP_IMPORT_COMPLETE_MESSAGE"; +"BACKUP_IMPORT_COMPLETE_MESSAGE" = "Restore Complete"; /* Label for button confirming backup import. */ "BACKUP_IMPORT_CONFIRM_ALERT_BUTTON" = "Restore"; @@ -190,26 +190,17 @@ /* Title for alert confirming backup import. */ "BACKUP_IMPORT_CONFIRM_ALERT_TITLE" = "Restore Backup?"; -/* Label for button that copies backup password to the pasteboard. */ -"BACKUP_IMPORT_COPY_PASSWORD_BUTTON" = "BACKUP_IMPORT_COPY_PASSWORD_BUTTON"; - /* Message indicating that backup import failed. */ -"BACKUP_IMPORT_FAILED_MESSAGE" = "BACKUP_IMPORT_FAILED_MESSAGE"; +"BACKUP_IMPORT_FAILED_MESSAGE" = "Restore Failed"; /* Message indicating that backup import is in progress. */ -"BACKUP_IMPORT_IN_PROGRESS_MESSAGE" = "BACKUP_IMPORT_IN_PROGRESS_MESSAGE"; +"BACKUP_IMPORT_IN_PROGRESS_MESSAGE" = "Restoring Backup..."; -/* Format for message indicating that backup import is complete. Embeds: {{the backup password}}. */ -"BACKUP_IMPORT_PASSWORD_MESSAGE_FORMAT" = "BACKUP_IMPORT_PASSWORD_MESSAGE_FORMAT"; - -/* Label for button that 'send backup' in the current conversation. */ -"BACKUP_IMPORT_SEND_BACKUP_BUTTON" = "BACKUP_IMPORT_SEND_BACKUP_BUTTON"; - -/* Label for button that opens share UI for backup. */ -"BACKUP_IMPORT_SHARE_BACKUP_BUTTON" = "BACKUP_IMPORT_SHARE_BACKUP_BUTTON"; +/* Label for button that restarts app to complete restore. */ +"BACKUP_IMPORT_RESTART_BUTTON" = "Restart App"; /* Title for the 'backup import' view. */ -"BACKUP_IMPORT_VIEW_TITLE" = "BACKUP_IMPORT_VIEW_TITLE"; +"BACKUP_IMPORT_VIEW_TITLE" = "Restore Backup"; /* An explanation of the consequences of blocking another user. */ "BLOCK_BEHAVIOR_EXPLANATION" = "Blocked users will not be able to call you or send you messages."; diff --git a/SignalServiceKit/src/Storage/OWSStorage.m b/SignalServiceKit/src/Storage/OWSStorage.m index 56f84d6535..1f37c9a287 100644 --- a/SignalServiceKit/src/Storage/OWSStorage.m +++ b/SignalServiceKit/src/Storage/OWSStorage.m @@ -556,24 +556,11 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass"; - (NSString *)createAndSetNewDatabasePassword { - NSString *newDBPassword = [[Randomness generateRandomBytes:30] base64EncodedString]; - NSError *keySetError; - [SAMKeychain setPassword:newDBPassword forService:keychainService account:keychainDBPassAccount error:&keySetError]; - if (keySetError) { - OWSProdCritical([OWSAnalyticsEvents storageErrorCouldNotStoreDatabasePassword]); + NSString *password = [[Randomness generateRandomBytes:30] base64EncodedString]; - [OWSStorage deletePasswordFromKeychain]; + [OWSStorage storeDatabasePassword:password]; - // Sleep to give analytics events time to be delivered. - [NSThread sleepForTimeInterval:15.0f]; - - [NSException raise:OWSStorageExceptionName_DatabasePasswordUnwritable - format:@"Setting DB password failed with error: %@", keySetError]; - } else { - DDLogWarn(@"Succesfully set new DB password."); - } - - return newDBPassword; + return password; } - (void)backgroundedAppDatabasePasswordInaccessibleWithErrorDescription:(NSString *)errorDescription @@ -610,6 +597,27 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass"; return fileSize; } ++ (void)storeDatabasePassword:(NSString *)password +{ + NSError *error; + [SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly]; + BOOL success = + [SAMKeychain setPassword:password forService:keychainService account:keychainDBPassAccount error:&error]; + if (!success || error) { + OWSProdCritical([OWSAnalyticsEvents storageErrorCouldNotStoreDatabasePassword]); + + [OWSStorage deletePasswordFromKeychain]; + + // Sleep to give analytics events time to be delivered. + [NSThread sleepForTimeInterval:15.0f]; + + [NSException raise:OWSStorageExceptionName_DatabasePasswordUnwritable + format:@"Setting DB password failed with error: %@", error]; + } else { + DDLogWarn(@"Succesfully set new DB password."); + } +} + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Util/OWSFileSystem.h b/SignalServiceKit/src/Util/OWSFileSystem.h index 54360d7c64..fceb6f7fe9 100644 --- a/SignalServiceKit/src/Util/OWSFileSystem.h +++ b/SignalServiceKit/src/Util/OWSFileSystem.h @@ -8,7 +8,8 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; -+ (void)protectFileOrFolderAtPath:(NSString *)path; +// TODO: We shouldn't ignore the return value of this method. ++ (BOOL)protectFileOrFolderAtPath:(NSString *)path; + (NSString *)appDocumentDirectoryPath; diff --git a/SignalServiceKit/src/Util/OWSFileSystem.m b/SignalServiceKit/src/Util/OWSFileSystem.m index 0016283d61..d892f57f92 100644 --- a/SignalServiceKit/src/Util/OWSFileSystem.m +++ b/SignalServiceKit/src/Util/OWSFileSystem.m @@ -9,10 +9,10 @@ NS_ASSUME_NONNULL_BEGIN @implementation OWSFileSystem -+ (void)protectFileOrFolderAtPath:(NSString *)path ++ (BOOL)protectFileOrFolderAtPath:(NSString *)path { if (![NSFileManager.defaultManager fileExistsAtPath:path]) { - return; + return NO; } NSError *error; @@ -26,7 +26,9 @@ NS_ASSUME_NONNULL_BEGIN if (error || !success) { OWSProdCritical([OWSAnalyticsEvents storageErrorFileProtection]); + return NO; } + return YES; } + (NSString *)appDocumentDirectoryPath @@ -139,6 +141,7 @@ NS_ASSUME_NONNULL_BEGIN NSArray *filenames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dirPath error:error]; if (*error) { + OWSFail(@"%@ could not find files in directory: %@", self.logTag, *error); return nil; } From f6296f1fe2d55889503d18e90171cf9d1a86aeb0 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 9 Jan 2018 10:14:57 -0500 Subject: [PATCH 11/15] Clean up ahead of PR. --- Signal/src/util/OWSBackup.m | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Signal/src/util/OWSBackup.m b/Signal/src/util/OWSBackup.m index a08e3663b4..44958f4b72 100644 --- a/Signal/src/util/OWSBackup.m +++ b/Signal/src/util/OWSBackup.m @@ -18,7 +18,7 @@ NS_ASSUME_NONNULL_BEGIN // Hide the "import" directories from exports, etc. by prefixing their name with a period. NSString *const OWSBackup_DirNamePrefix = @".SignalBackup."; NSString *const OWSBackup_FileExtension = @".signalbackup"; -NSString *const OWSBackup_EncryptionKeyFilename = @"OWSBackup_EncryptionKeyFilename"; +NSString *const OWSBackup_EncryptionKeyFilename = @".encryptionKey"; NSString *const OWSBackup_DatabasePasswordFilename = @".databasePassword"; NSString *const OWSBackup_StandardUserDefaultsFilename = @".standardUserDefaults"; NSString *const OWSBackup_AppUserDefaultsFilename = @".appUserDefaults"; @@ -172,6 +172,9 @@ NSString *const Keychain_ImportBackupKey = @"ImportBackupKey"; { DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + // First, clean up any existing backup import/export state. + [OWSBackup cleanupBackupState]; + NSString *temporaryDirectory = NSTemporaryDirectory(); NSString *rootDirName = [OWSBackup_DirNamePrefix stringByAppendingString:[NSUUID UUID].UUIDString]; NSString *rootDirPath = [temporaryDirectory stringByAppendingPathComponent:rootDirName]; From 0422e4252fe70707a3edba78cd437965cf7ec460 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 9 Jan 2018 10:23:42 -0500 Subject: [PATCH 12/15] Clean up ahead of PR. --- Signal/src/util/OWSBackup.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Signal/src/util/OWSBackup.h b/Signal/src/util/OWSBackup.h index d6e50a7beb..bfafe6bae0 100644 --- a/Signal/src/util/OWSBackup.h +++ b/Signal/src/util/OWSBackup.h @@ -25,6 +25,21 @@ typedef NS_ENUM(NSUInteger, OWSBackupState) { @class TSThread; +// We restore backups as part of the app launch process. +// +// applicationDidFinishLaunching must complete quickly even for +// large backups, to prevent the app from being killed on launch. +// Therefore, we break up backup import/restoration into two parts: +// +// * Preparation (which includes the costly decryption/unzip of the +// backup file) +// * Completion (file moves, NSUserDefaults writes, keychain writes). +// +// To protect data during backup and restore, we: +// +// * Optionally encrypt backup files with a password. +// * Separately encrypt files containing keychain & NSUserDefaults data. +// * Delete data from disk ASAP. @interface OWSBackup : NSObject @property (nonatomic, weak) id delegate; From df53033cafc755a4392f4437fc39154fe545f18d Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 9 Jan 2018 12:29:22 -0500 Subject: [PATCH 13/15] Clean up ahead of PR. --- Signal/src/util/OWSBackup.h | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Signal/src/util/OWSBackup.h b/Signal/src/util/OWSBackup.h index bfafe6bae0..48574c7708 100644 --- a/Signal/src/util/OWSBackup.h +++ b/Signal/src/util/OWSBackup.h @@ -44,13 +44,20 @@ typedef NS_ENUM(NSUInteger, OWSBackupState) { @property (nonatomic, weak) id delegate; +// An instance of `OWSBackup` is used for three separate tasks: +// +// * Backup export +// * Backup import preparation +// * Backup import completion +// +// The "backup state" and "progress" apply to all three tasks. @property (nonatomic, readonly) OWSBackupState backupState; - @property (nonatomic, readonly) CGFloat backupProgress; // If non-nil, backup is encrypted. @property (nonatomic, nullable, readonly) NSString *backupPassword; +// Only applies to "backup export" task. @property (nonatomic, nullable, readonly) TSThread *currentThread; @property (nonatomic, readonly) NSString *backupZipPath; From 5ba8445f05e8790e724167954d0a1dad0ca46ba8 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 11 Jan 2018 10:46:28 -0500 Subject: [PATCH 14/15] Fix rebase breakage. --- Signal/src/util/OWSBackup.m | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Signal/src/util/OWSBackup.m b/Signal/src/util/OWSBackup.m index 44958f4b72..e2828eb4bf 100644 --- a/Signal/src/util/OWSBackup.m +++ b/Signal/src/util/OWSBackup.m @@ -198,7 +198,7 @@ NSString *const Keychain_ImportBackupKey = @"ImportBackupKey"; DDLogInfo(@"%@ backupZipPath: %@", self.logTag, backupZipPath); [OWSFileSystem ensureDirectoryExists:rootDirPath]; - [OWSFileSystem protectFolderAtPath:rootDirPath]; + [OWSFileSystem protectFileOrFolderAtPath:rootDirPath]; [OWSFileSystem ensureDirectoryExists:backupDirPath]; if (self.isCancelledOrFailed) { @@ -267,7 +267,7 @@ NSString *const Keychain_ImportBackupKey = @"ImportBackupKey"; return [self fail]; } - [OWSFileSystem protectFolderAtPath:backupZipPath]; + [OWSFileSystem protectFileOrFolderAtPath:backupZipPath]; [OWSFileSystem deleteFileIfExists:self.backupDirPath]; } @@ -486,7 +486,7 @@ NSString *const Keychain_ImportBackupKey = @"ImportBackupKey"; DDLogInfo(@"%@ backupZipPath: %@", self.logTag, backupZipPath); [OWSFileSystem ensureDirectoryExists:rootDirPath]; - [OWSFileSystem protectFolderAtPath:rootDirPath]; + [OWSFileSystem protectFileOrFolderAtPath:rootDirPath]; [OWSFileSystem ensureDirectoryExists:backupDirPath]; NSError *error = nil; @@ -799,7 +799,7 @@ NSString *const Keychain_ImportBackupKey = @"ImportBackupKey"; OWSFail(@"%@ failed to move directory item: %@, %@", self.logTag, dstFilePath, error); return NO; } - if (![OWSFileSystem protectFolderAtPath:oldFilePath]) { + if (![OWSFileSystem protectFileOrFolderAtPath:oldFilePath]) { OWSFail(@"%@ failed to protect old directory item: %@, %@", self.logTag, oldFilePath, error); return NO; } @@ -810,7 +810,7 @@ NSString *const Keychain_ImportBackupKey = @"ImportBackupKey"; OWSFail(@"%@ failed to move directory item: %@, %@", self.logTag, dstFilePath, error); return NO; } - if (![OWSFileSystem protectFolderAtPath:dstFilePath]) { + if (![OWSFileSystem protectFileOrFolderAtPath:dstFilePath]) { OWSFail(@"%@ failed to protect directory item: %@, %@", self.logTag, dstFilePath, error); return NO; } From 59933ce1d69ebf1d41e2497d19bce9f16e769ca6 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 11 Jan 2018 11:15:50 -0500 Subject: [PATCH 15/15] Fix rebase breakage. --- Signal/src/AppDelegate.m | 6 +++--- Signal/src/ViewControllers/DebugUI/DebugUIMisc.m | 9 --------- .../ViewControllers/OWSBackupExportViewController.m | 1 + Signal/src/util/OWSBackup.m | 13 +++++++++++-- SignalServiceKit/src/Util/OWSFileSystem.h | 2 +- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 313887e48b..72418628cc 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -130,13 +130,13 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; SetRandFunctionSeed(); + // XXX - careful when moving this. It must happen before we initialize TSStorageManager. + [self verifyDBKeysAvailableBeforeBackgroundLaunch]; + // If a backup restore is in progress, try to complete it. // Otherwise, cleanup backup state. [OWSBackup applicationDidFinishLaunching]; - // XXX - careful when moving this. It must happen before we initialize TSStorageManager. - [self verifyDBKeysAvailableBeforeBackgroundLaunch]; - // Prevent the device from sleeping during database view async registration // (e.g. long database upgrades). // diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m index 30cc540e08..56ecea7108 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m @@ -95,15 +95,6 @@ NS_ASSUME_NONNULL_BEGIN [DebugUIMisc sendUnencryptedDatabase:thread]; }]]; } - [items addObject:[OWSTableItem - subPageItemWithText:@"Export Backup w/ Password" - actionBlock:^(UIViewController *viewController) { - OWSBackupExportViewController *backupViewController = - [OWSBackupExportViewController new]; - [backupViewController exportBackup:thread skipPassword:NO]; - [viewController.navigationController pushViewController:backupViewController - animated:YES]; - }]]; [items addObject:[OWSTableItem subPageItemWithText:@"Export Backup w/o Password" actionBlock:^(UIViewController *viewController) { diff --git a/Signal/src/ViewControllers/OWSBackupExportViewController.m b/Signal/src/ViewControllers/OWSBackupExportViewController.m index 82d169e71d..bebd6165f1 100644 --- a/Signal/src/ViewControllers/OWSBackupExportViewController.m +++ b/Signal/src/ViewControllers/OWSBackupExportViewController.m @@ -217,6 +217,7 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssert(self.backup.backupPassword.length > 0); + // TODO: We could consider clearing the password from the pasteboard after a certain delay. [UIPasteboard.generalPasteboard setString:self.backup.backupPassword]; } diff --git a/Signal/src/util/OWSBackup.m b/Signal/src/util/OWSBackup.m index e2828eb4bf..cf6ae7b2c2 100644 --- a/Signal/src/util/OWSBackup.m +++ b/Signal/src/util/OWSBackup.m @@ -667,6 +667,10 @@ NSString *const Keychain_ImportBackupKey = @"ImportBackupKey"; } self.encryptionKey = [OWSAES256Key keyWithData:encryptionKeyData]; + if (![self isValidBackup]) { + return; + } + NSData *_Nullable databasePassword = [self readDataFromFileName:OWSBackup_DatabasePasswordFilename]; if (!databasePassword) { OWSFail(@"%@ Could not retrieve database password.", self.logTag); @@ -771,6 +775,10 @@ NSString *const Keychain_ImportBackupKey = @"ImportBackupKey"; DDLogInfo(@"%@ restoreDirectoryContents: %@ -> %@", self.logTag, srcDirPath, dstDirPath); if (![[NSFileManager defaultManager] fileExistsAtPath:srcDirPath]) { + // Not all backups will have both a "app documents" and "shared data container" folder. + // The latter should always be present for "modern" installs, but we are permissive + // here about what we accept so that we can easily apply this branch to historic + // (pre-shared data container) versions of the app and restore from them. DDLogInfo(@"%@ Skipping restore directory: %@.", self.logTag, srcDirPath); return YES; } @@ -861,9 +869,10 @@ NSString *const Keychain_ImportBackupKey = @"ImportBackupKey"; - (NSString *)relativePathforPath:(NSString *)filePath basePath:(NSString *)basePath { OWSAssert(filePath.stringByStandardizingPath.length > 0); - OWSAssert([filePath.stringByStandardizingPath hasPrefix:basePath]); + OWSAssert([filePath.stringByStandardizingPath hasPrefix:basePath.stringByStandardizingPath]); - NSString *relativePath = [filePath.stringByStandardizingPath substringFromIndex:basePath.length]; + NSString *relativePath = + [filePath.stringByStandardizingPath substringFromIndex:basePath.stringByStandardizingPath.length]; NSString *separator = @"/"; if ([relativePath hasPrefix:separator]) { relativePath = [relativePath substringFromIndex:separator.length]; diff --git a/SignalServiceKit/src/Util/OWSFileSystem.h b/SignalServiceKit/src/Util/OWSFileSystem.h index fceb6f7fe9..20903497b6 100644 --- a/SignalServiceKit/src/Util/OWSFileSystem.h +++ b/SignalServiceKit/src/Util/OWSFileSystem.h @@ -9,7 +9,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; // TODO: We shouldn't ignore the return value of this method. -+ (BOOL)protectFileOrFolderAtPath:(NSString *)path; ++ (BOOL)protectFileOrFolderAtPath:(NSString *)path __attribute__((warn_unused_result)); + (NSString *)appDocumentDirectoryPath;