Detect, warn about and try to recover from database password retrieval and database load errors.

// FREEBIE
pull/1/head
Matthew Chen 8 years ago
parent 87719a3bfb
commit c5cf79c399

@ -24,7 +24,7 @@ extern NSString *const TSUIDatabaseConnectionDidUpdateNotification;
- (void)setupDatabase; - (void)setupDatabase;
- (void)deleteThreadsAndMessages; - (void)deleteThreadsAndMessages;
- (BOOL)databasePasswordAccessible; - (BOOL)databasePasswordAccessible;
- (void)wipeSignalStorage; - (void)resetSignalStorage;
- (YapDatabase *)database; - (YapDatabase *)database;
- (YapDatabaseConnection *)newDatabaseConnection; - (YapDatabaseConnection *)newDatabaseConnection;

@ -1,8 +1,12 @@
// Created by Frederic Jacobs on 27/10/14. //
// Copyright (c) 2014 Open Whisper Systems. All rights reserved. // TSStorageManager.m
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "TSStorageManager.h" #import "TSStorageManager.h"
#import "NSData+Base64.h" #import "NSData+Base64.h"
#import "OWSAnalytics.h"
#import "OWSDisappearingMessagesFinder.h" #import "OWSDisappearingMessagesFinder.h"
#import "OWSReadReceipt.h" #import "OWSReadReceipt.h"
#import "SignalRecipient.h" #import "SignalRecipient.h"
@ -32,18 +36,24 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
@end @end
#pragma mark -
// Some lingering TSRecipient records in the wild causing crashes. // Some lingering TSRecipient records in the wild causing crashes.
// This is a stop gap until a proper cleanup happens. // This is a stop gap until a proper cleanup happens.
@interface TSRecipient : NSObject <NSCoding> @interface TSRecipient : NSObject <NSCoding>
@end @end
#pragma mark -
@interface OWSUnknownObject : NSObject <NSCoding> @interface OWSUnknownObject : NSObject <NSCoding>
@end @end
#pragma mark -
/** /**
* A default object to returned when we can't deserialize an object from YapDB. This can prevent crashes when * A default object to return when we can't deserialize an object from YapDB. This can prevent crashes when
* old objects linger after their definition file is removed. The danger is that, the objects can lay in wait * old objects linger after their definition file is removed. The danger is that, the objects can lay in wait
* until the next time a DB extension is added and we necessarily enumerate the entire DB. * until the next time a DB extension is added and we necessarily enumerate the entire DB.
*/ */
@ -61,10 +71,14 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
@end @end
#pragma mark -
@interface OWSUnarchiverDelegate : NSObject <NSKeyedUnarchiverDelegate> @interface OWSUnarchiverDelegate : NSObject <NSKeyedUnarchiverDelegate>
@end @end
#pragma mark -
@implementation OWSUnarchiverDelegate @implementation OWSUnarchiverDelegate
- (nullable Class)unarchiver:(NSKeyedUnarchiver *)unarchiver cannotDecodeObjectOfClassName:(NSString *)name originalClasses:(NSArray<NSString *> *)classNames - (nullable Class)unarchiver:(NSKeyedUnarchiver *)unarchiver cannotDecodeObjectOfClassName:(NSString *)name originalClasses:(NSArray<NSString *> *)classNames
@ -75,6 +89,8 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
@end @end
#pragma mark -
@implementation TSStorageManager @implementation TSStorageManager
+ (instancetype)sharedManager { + (instancetype)sharedManager {
@ -93,10 +109,39 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
{ {
self = [super init]; self = [super init];
if (![self tryToLoadDatabase]) {
// Failing to load the database is catastrophic.
//
// The best we can try to do is to discard the current database
// and behave like a clean install.
OWSAnalyticsCritical(@"Could not load database");
// Try to reset app by deleting database.
[self resetSignalStorage];
if (![self tryToLoadDatabase]) {
OWSAnalyticsCritical(@"Could not load database (second attempt)");
[NSException raise:TSStorageManagerExceptionNameNoDatabase format:@"Failed to initialize database."];
}
}
return self;
}
- (BOOL)tryToLoadDatabase
{
// We determine the database password first, since a side effect of
// this can be deleting any existing database file (if we're recovering
// from a corrupt keychain).
NSData *databasePassword = [self databasePassword];
YapDatabaseOptions *options = [[YapDatabaseOptions alloc] init]; YapDatabaseOptions *options = [[YapDatabaseOptions alloc] init];
options.corruptAction = YapDatabaseCorruptAction_Fail; options.corruptAction = YapDatabaseCorruptAction_Fail;
options.cipherKeyBlock = ^{ options.cipherKeyBlock = ^{
return [self databasePassword]; return databasePassword;
}; };
_database = [[YapDatabase alloc] initWithPath:[self dbPath] _database = [[YapDatabase alloc] initWithPath:[self dbPath]
@ -104,12 +149,11 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
deserializer:[[self class] logOnFailureDeserializer] deserializer:[[self class] logOnFailureDeserializer]
options:options]; options:options];
if (!_database) { if (!_database) {
DDLogError(@"%@ Failed to initialize database.", self.tag); return NO;
[NSException raise:TSStorageManagerExceptionNameNoDatabase format:@"Failed to initialize database."];
} }
_dbConnection = self.newDatabaseConnection; _dbConnection = self.newDatabaseConnection;
return self; return YES;
} }
/** /**
@ -252,21 +296,26 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
[SAMKeychain passwordForService:keychainService account:keychainDBPassAccount error:&keyFetchError]; [SAMKeychain passwordForService:keychainService account:keychainDBPassAccount error:&keyFetchError];
if (keyFetchError) { if (keyFetchError) {
switch (keyFetchError.code) { // Either this is a new install so there's no existing password to retrieve
case errSecItemNotFound: // or the keychain has become corrupt. Either way, we want to get back to a
dbPassword = [self createAndSetNewDatabasePassword]; // "known good state" and behave like a new install.
break;
default: BOOL shouldHavePassword = [NSFileManager.defaultManager fileExistsAtPath:[self dbPath]];
DDLogError(@"%@ Getting DB password from keychain failed with error: %@", self.tag, keyFetchError); if (shouldHavePassword) {
[NSException raise:TSStorageManagerExceptionNameDatabasePasswordInaccessible OWSAnalyticsCriticalWithParameters(@"Could not retrieve database password from keychain",
format:@"Getting DB password from keychain failed with error: %@", keyFetchError]; @{ @"ErrorCode" : @(keyFetchError.code) });
break;
} }
// Try to reset app by deleting database.
[self resetSignalStorage];
dbPassword = [self createAndSetNewDatabasePassword];
} }
return [dbPassword dataUsingEncoding:NSUTF8StringEncoding]; return [dbPassword dataUsingEncoding:NSUTF8StringEncoding];
} }
- (NSString *)createAndSetNewDatabasePassword - (NSString *)createAndSetNewDatabasePassword
{ {
NSString *newDBPassword = [[Randomness generateRandomBytes:30] base64EncodedString]; NSString *newDBPassword = [[Randomness generateRandomBytes:30] base64EncodedString];
@ -274,16 +323,19 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
[SAMKeychain setPassword:newDBPassword forService:keychainService account:keychainDBPassAccount error:&keySetError]; [SAMKeychain setPassword:newDBPassword forService:keychainService account:keychainDBPassAccount error:&keySetError];
if (keySetError) { if (keySetError) {
DDLogError(@"%@ Setting DB password failed with error: %@", self.tag, keySetError); DDLogError(@"%@ Setting DB password failed with error: %@", self.tag, keySetError);
[self deletePasswordFromKeychain];
[NSException raise:TSStorageManagerExceptionNameDatabasePasswordUnwritable [NSException raise:TSStorageManagerExceptionNameDatabasePasswordUnwritable
format:@"Setting DB password failed with error: %@", keySetError]; format:@"Setting DB password failed with error: %@", keySetError];
} else { } else {
DDLogError(@"Succesfully set new DB password. First launch?"); DDLogError(@"Succesfully set new DB password.");
} }
return newDBPassword; return newDBPassword;
} }
#pragma mark convenience methods #pragma mark - convenience methods
- (void)purgeCollection:(NSString *)collection { - (void)purgeCollection:(NSString *)collection {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
@ -378,21 +430,30 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
[TSAttachmentStream deleteAttachments]; [TSAttachmentStream deleteAttachments];
} }
- (void)wipeSignalStorage { - (void)deletePasswordFromKeychain
self.database = nil; {
NSError *error;
[SAMKeychain deletePasswordForService:keychainService account:keychainDBPassAccount]; [SAMKeychain deletePasswordForService:keychainService account:keychainDBPassAccount];
[[NSFileManager defaultManager] removeItemAtPath:[self dbPath] error:&error]; }
- (void)deleteDatabaseFile
{
NSError *error;
[[NSFileManager defaultManager] removeItemAtPath:[self dbPath] error:&error];
if (error) { if (error) {
DDLogError(@"Failed to delete database: %@", error.description); DDLogError(@"Failed to delete database: %@", error.description);
} }
}
[TSAttachmentStream deleteAttachments]; - (void)resetSignalStorage
{
self.database = nil;
_dbConnection = nil;
[self deletePasswordFromKeychain];
[[self init] setupDatabase]; [self deleteDatabaseFile];
[TSAttachmentStream deleteAttachments];
} }
#pragma mark - Logging #pragma mark - Logging

Loading…
Cancel
Save