diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 5bbd8ebf1..313887e48 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 13da6516a..8774344da 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 8e06f71a9..63d7dc82f 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 bc6f6c8e7..9ae85caab 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 e58db16b3..a91c38c85 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 69247f72b..69b2b310f 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 ce462ecb3..d6e50a7be 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 5eb984de2..a08e3663b 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; - // - // 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]; +- (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: // - // [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 adaa5fff2..a77024537 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"; - -/* Format for message indicating that backup import is complete. Embeds: {{the backup password}}. */ -"BACKUP_IMPORT_PASSWORD_MESSAGE_FORMAT" = "BACKUP_IMPORT_PASSWORD_MESSAGE_FORMAT"; +"BACKUP_IMPORT_IN_PROGRESS_MESSAGE" = "Restoring Backup..."; -/* 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 56f84d653..1f37c9a28 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 54360d7c6..fceb6f7fe 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 0016283d6..d892f57f9 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; }