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..acf560a9f0 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, ); }; }; @@ -117,8 +118,11 @@ 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 */; }; 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 */; }; @@ -597,12 +601,18 @@ 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 = ""; }; 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 = ""; }; @@ -1320,6 +1330,10 @@ 34B3F8591E8DF1700035BE1A /* NotificationSettingsViewController.m */, 34CCAF391F0C2748004084F4 /* OWSAddToContactViewController.h */, 34CCAF3A1F0C2748004084F4 /* OWSAddToContactViewController.m */, + 347E0B772003CD7400BC2F76 /* OWSBackupExportViewController.h */, + 347E0B762003CD7400BC2F76 /* OWSBackupExportViewController.m */, + 347E0B792003CD7500BC2F76 /* OWSBackupImportViewController.h */, + 347E0B782003CD7400BC2F76 /* OWSBackupImportViewController.m */, 34B3F85B1E8DF1700035BE1A /* OWSConversationSettingsViewController.h */, 34B3F85C1E8DF1700035BE1A /* OWSConversationSettingsViewController.m */, 34D5CCAB1EAE7136005515DB /* OWSConversationSettingsViewDelegate.h */, @@ -1703,8 +1717,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 +1731,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 +2543,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 +2569,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", @@ -2893,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 */, @@ -2927,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 */, @@ -2945,6 +2965,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 */, diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 5bbd8ebf14..72418628cc 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" @@ -132,6 +133,10 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; // 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]; + // Prevent the device from sleeping during database view async registration // (e.g. long database upgrades). // diff --git a/Signal/src/ViewControllers/AppSettingsViewController.m b/Signal/src/ViewControllers/AppSettingsViewController.m index 1a93ab87aa..8774344dab 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" diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 7bd7c77229..63d7dc82f6 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,42 @@ 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]; + UINavigationController *navigationController = + [[UINavigationController alloc] initWithRootViewController:backupViewController]; + UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController]; + [fromViewController presentViewController:navigationController 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 9a46423df9..56ecea7108 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m @@ -3,6 +3,8 @@ // #import "DebugUIMisc.h" +#import "OWSBackup.h" +#import "OWSBackupExportViewController.h" #import "OWSCountryMetadata.h" #import "OWSTableViewController.h" #import "RegistrationViewController.h" @@ -93,6 +95,15 @@ NS_ASSUME_NONNULL_BEGIN [DebugUIMisc sendUnencryptedDatabase:thread]; }]]; } + [items addObject:[OWSTableItem + subPageItemWithText:@"Export Backup w/o Password" + actionBlock:^(UIViewController *viewController) { + OWSBackupExportViewController *backupViewController = + [OWSBackupExportViewController 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/OWSBackupExportViewController.h b/Signal/src/ViewControllers/OWSBackupExportViewController.h new file mode 100644 index 0000000000..ef5586dcd2 --- /dev/null +++ b/Signal/src/ViewControllers/OWSBackupExportViewController.h @@ -0,0 +1,19 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class TSThread; + +@interface OWSBackupExportViewController : 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/OWSBackupExportViewController.m b/Signal/src/ViewControllers/OWSBackupExportViewController.m new file mode 100644 index 0000000000..bebd6165f1 --- /dev/null +++ b/Signal/src/ViewControllers/OWSBackupExportViewController.m @@ -0,0 +1,312 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSBackupExportViewController.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 OWSBackupExportViewController () + +@property (nonatomic) OWSBackup *backup; + +@property (nonatomic, nullable) OWSProgressView *progressView; + +@end + +#pragma mark - + +@implementation OWSBackupExportViewController + +- (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]; + } + 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_EXPORT_IN_PROGRESS_MESSAGE", @"Message indicating that backup export 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_EXPORT_COMPLETE_MESSAGE", @"Message indicating that backup export 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 " + @"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]; +} + +- (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; + 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); + + // TODO: We could consider clearing the password from the pasteboard after a certain delay. + [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]; +} + +- (void)backupProgressDidChange +{ + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + + self.progressView.progress = self.backup.backupProgress; +} + +@end + +NS_ASSUME_NONNULL_END 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..a91c38c858 --- /dev/null +++ b/Signal/src/ViewControllers/OWSBackupImportViewController.m @@ -0,0 +1,217 @@ +// +// 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]; + } + + [subviews addObject:[UIView new]]; + + [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]; + [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 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 + +- (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/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 new file mode 100644 index 0000000000..48574c7708 --- /dev/null +++ b/Signal/src/util/OWSBackup.h @@ -0,0 +1,75 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const OWSBackup_FileExtension; + +@protocol OWSBackupDelegate + +- (void)backupStateDidChange; + +- (void)backupProgressDidChange; + +@end + +#pragma mark - + +typedef NS_ENUM(NSUInteger, OWSBackupState) { + OWSBackupState_InProgress, + OWSBackupState_Cancelled, + OWSBackupState_Complete, + OWSBackupState_Failed, +}; + +@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; + +// 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; + +- (void)exportBackup:(nullable TSThread *)currentThread skipPassword:(BOOL)skipPassword; + +- (void)importBackup:(NSString *)backupZipPath password:(NSString *_Nullable)password; + +- (void)cancel; + ++ (void)applicationDidFinishLaunching; + +@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..cf6ae7b2c2 --- /dev/null +++ b/Signal/src/util/OWSBackup.m @@ -0,0 +1,917 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSBackup.h" +#import "NSUserDefaults+OWS.h" +#import "Signal-Swift.h" +#import "zlib.h" +#import +#import +#import +#import +#import +#import + +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 = @".encryptionKey"; +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 () + +@property (nonatomic) OWSBackupState backupState; + +@property (nonatomic) CGFloat backupProgress; + +@property (nonatomic, nullable) TSThread *currentThread; + +@property (nonatomic, nullable) NSString *backupPassword; + +@property (nonatomic) NSString *backupDirPath; +@property (nonatomic) NSString *backupZipPath; + +@property (nonatomic) OWSAES256Key *encryptionKey; + +@end + +#pragma mark - + +@implementation OWSBackup + +- (void)dealloc +{ + 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; + + 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; + } +} + +- (BOOL)isCancelledOrFailed +{ + 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); + + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + + self.currentThread = currentThread; + self.backupState = OWSBackupState_InProgress; + + if (skipPassword) { + 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; + DDLogInfo(@"%@ backup export with password: %@", self.logTag, backupPassword); + } + + [self startExport]; +} + +- (void)startExport +{ + OWSAssertIsOnMainThread(); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self exportToFilesAndZip]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self complete]; + }); + }); +} + +- (void)exportToFilesAndZip +{ + 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]; + NSString *backupDirPath = [rootDirPath stringByAppendingPathComponent:@"Contents"]; + + 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:OWSBackup_FileExtension]]; + 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 protectFileOrFolderAtPath:rootDirPath]; + [OWSFileSystem ensureDirectoryExists:backupDirPath]; + + if (self.isCancelledOrFailed) { + return; + } + + 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:OWSBackup_DatabasePasswordFilename + backupDirPath:backupDirPath + encryptionKey:encryptionKey]) { + return [self fail]; + } + if (self.isCancelledOrFailed) { + return; + } + if (![self writeUserDefaults:NSUserDefaults.standardUserDefaults + fileName:OWSBackup_StandardUserDefaultsFilename + backupDirPath:backupDirPath + encryptionKey:encryptionKey]) { + return [self fail]; + } + if (self.isCancelledOrFailed) { + return; + } + if (![self writeUserDefaults:NSUserDefaults.appUserDefaults + fileName:OWSBackup_AppUserDefaultsFilename + backupDirPath:backupDirPath + encryptionKey:encryptionKey]) { + 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:OWSBackup_AppDocumentDirName + backupDirPath:backupDirPath]) { + [self fail]; + return; + } + if (self.isCancelledOrFailed) { + return; + } + if (![self copyDirectory:OWSFileSystem.appSharedDataDirectoryPath + dstDirName:OWSBackup_AppSharedDataDirName + backupDirPath:backupDirPath]) { + [self fail]; + return; + } + }]; + if (self.isCancelledOrFailed) { + return; + } + if (![self zipDirectory:backupDirPath dstFilePath:backupZipPath encryptionKey:encryptionKey]) { + return [self fail]; + } + + [OWSFileSystem protectFileOrFolderAtPath:backupZipPath]; + + [OWSFileSystem deleteFileIfExists:self.backupDirPath]; +} + +- (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 *_Nullable encryptedData = [Cryptography encryptAESGCMWithData:data key:encryptionKey]; + if (!encryptedData) { + OWSFail(@"%@ failed to encrypt data: %@", self.logTag, fileName); + return NO; + } + + NSString *filePath = [backupDirPath stringByAppendingPathComponent:fileName]; + + DDLogInfo(@"%@ writeData: %@", self.logTag, filePath); + + NSError *error; + BOOL success = [encryptedData 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]; + + 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 + // contains files that the app is not allowed to access. + [OWSFileSystem ensureDirectoryExists:dstDirPath]; + 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 ([fileName hasPrefix:@"."]) { + DDLogInfo(@"%@ 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; +} + +- (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); + + DDLogInfo(@"%@ writeUserDefaults: %@", self.logTag, fileName); + + 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 encryptionKey:encryptionKey]; +} + +- (BOOL)zipDirectory:(NSString *)srcDirPath + dstFilePath:(NSString *)dstFilePath + encryptionKey:(OWSAES256Key *)encryptionKey +{ + OWSAssert(srcDirPath.length > 0); + OWSAssert(dstFilePath.length > 0); + OWSAssert(encryptionKey); + + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + + srcDirPath = [srcDirPath stringByStandardizingPath]; + OWSAssert(srcDirPath.length > 0); + + 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; + } + + // 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) { + NSString *relativePath = [self relativePathforPath:srcFilePath basePath:srcDirPath]; + 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; + } + + NSNumber *fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:dstFilePath error:&error][NSFileSize]; + if (error) { + OWSFail(@"%@ failed to get zip file size: %@", self.logTag, error); + return NO; + } + DDLogInfo(@"%@ Zip file size: %@", self.logTag, fileSize); + + return YES; +} + +#pragma mark - Import Backup, Part 1 + +- (void)importBackup:(NSString *)srcZipPath password:(NSString *_Nullable)password +{ + OWSAssertIsOnMainThread(); + OWSAssert(srcZipPath.length > 0); + OWSAssert(CurrentAppContext().isMainApp); + + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + + self.backupPassword = password; + + self.backupState = OWSBackupState_InProgress; + + if (password.length == 0) { + DDLogInfo(@"%@ backup import without password", self.logTag); + } else { + DDLogInfo(@"%@ backup import with password: %@", self.logTag, password); + } + + [self startImport:srcZipPath]; +} + +- (void)startImport:(NSString *)srcZipPath +{ + OWSAssertIsOnMainThread(); + OWSAssert(srcZipPath.length > 0); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self prepareForImport:srcZipPath]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self complete]; + }); + }); +} + +- (void)prepareForImport:(NSString *)srcZipPath +{ + OWSAssert(srcZipPath.length > 0); + + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + + NSString *documentDirectoryPath = OWSFileSystem.appDocumentDirectoryPath; + NSString *rootDirName = [OWSBackup_DirNamePrefix 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 protectFileOrFolderAtPath: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; + } + 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]; + } +} + +- (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]; + + if (![self isValidBackup]) { + return; + } + + 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: + // + // * 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]) { + // 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; + } + + 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 protectFileOrFolderAtPath: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 protectFileOrFolderAtPath: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.stringByStandardizingPath]); + + NSString *relativePath = + [filePath.stringByStandardizingPath substringFromIndex:basePath.stringByStandardizingPath.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 + +NS_ASSUME_NONNULL_END diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index e93e381b15..a770245370 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -145,6 +145,63 @@ /* action sheet button title to enable built in speaker during a call */ "AUDIO_ROUTE_BUILT_IN_SPEAKER" = "Speaker"; +/* 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 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 %@"; + +/* Message indicating that backup import is complete. */ +"BACKUP_IMPORT_COMPLETE_MESSAGE" = "Restore Complete"; + +/* 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?"; + +/* Message indicating that backup import failed. */ +"BACKUP_IMPORT_FAILED_MESSAGE" = "Restore Failed"; + +/* Message indicating that backup import is in progress. */ +"BACKUP_IMPORT_IN_PROGRESS_MESSAGE" = "Restoring Backup..."; + +/* 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" = "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/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/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/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, ^{ diff --git a/SignalServiceKit/src/Util/OWSFileSystem.h b/SignalServiceKit/src/Util/OWSFileSystem.h index 898266fa2c..20903497b6 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 __attribute__((warn_unused_result)); + (NSString *)appDocumentDirectoryPath; @@ -27,6 +28,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..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 @@ -131,6 +133,38 @@ 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) { + OWSFail(@"%@ could not find files in directory: %@", self.logTag, *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