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)deleteThreadsAndMessages;
- (BOOL)databasePasswordAccessible;
- (void)wipeSignalStorage;
- (void)resetSignalStorage;
- (YapDatabase *)database;
- (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 "NSData+Base64.h"
#import "OWSAnalytics.h"
#import "OWSDisappearingMessagesFinder.h"
#import "OWSReadReceipt.h"
#import "SignalRecipient.h"
@ -32,18 +36,24 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
@end
#pragma mark -
// Some lingering TSRecipient records in the wild causing crashes.
// This is a stop gap until a proper cleanup happens.
@interface TSRecipient : NSObject <NSCoding>
@end
#pragma mark -
@interface OWSUnknownObject : NSObject <NSCoding>
@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
* 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
#pragma mark -
@interface OWSUnarchiverDelegate : NSObject <NSKeyedUnarchiverDelegate>
@end
#pragma mark -
@implementation OWSUnarchiverDelegate
- (nullable Class)unarchiver:(NSKeyedUnarchiver *)unarchiver cannotDecodeObjectOfClassName:(NSString *)name originalClasses:(NSArray<NSString *> *)classNames
@ -75,6 +89,8 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
@end
#pragma mark -
@implementation TSStorageManager
+ (instancetype)sharedManager {
@ -93,10 +109,39 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
{
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];
options.corruptAction = YapDatabaseCorruptAction_Fail;
options.cipherKeyBlock = ^{
return [self databasePassword];
options.cipherKeyBlock = ^{
return databasePassword;
};
_database = [[YapDatabase alloc] initWithPath:[self dbPath]
@ -104,12 +149,11 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
deserializer:[[self class] logOnFailureDeserializer]
options:options];
if (!_database) {
DDLogError(@"%@ Failed to initialize database.", self.tag);
[NSException raise:TSStorageManagerExceptionNameNoDatabase format:@"Failed to initialize database."];
return NO;
}
_dbConnection = self.newDatabaseConnection;
return self;
return YES;
}
/**
@ -252,21 +296,26 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
[SAMKeychain passwordForService:keychainService account:keychainDBPassAccount error:&keyFetchError];
if (keyFetchError) {
switch (keyFetchError.code) {
case errSecItemNotFound:
dbPassword = [self createAndSetNewDatabasePassword];
break;
default:
DDLogError(@"%@ Getting DB password from keychain failed with error: %@", self.tag, keyFetchError);
[NSException raise:TSStorageManagerExceptionNameDatabasePasswordInaccessible
format:@"Getting DB password from keychain failed with error: %@", keyFetchError];
break;
// Either this is a new install so there's no existing password to retrieve
// or the keychain has become corrupt. Either way, we want to get back to a
// "known good state" and behave like a new install.
BOOL shouldHavePassword = [NSFileManager.defaultManager fileExistsAtPath:[self dbPath]];
if (shouldHavePassword) {
OWSAnalyticsCriticalWithParameters(@"Could not retrieve database password from keychain",
@{ @"ErrorCode" : @(keyFetchError.code) });
}
// Try to reset app by deleting database.
[self resetSignalStorage];
dbPassword = [self createAndSetNewDatabasePassword];
}
return [dbPassword dataUsingEncoding:NSUTF8StringEncoding];
}
- (NSString *)createAndSetNewDatabasePassword
{
NSString *newDBPassword = [[Randomness generateRandomBytes:30] base64EncodedString];
@ -274,16 +323,19 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
[SAMKeychain setPassword:newDBPassword forService:keychainService account:keychainDBPassAccount error:&keySetError];
if (keySetError) {
DDLogError(@"%@ Setting DB password failed with error: %@", self.tag, keySetError);
[self deletePasswordFromKeychain];
[NSException raise:TSStorageManagerExceptionNameDatabasePasswordUnwritable
format:@"Setting DB password failed with error: %@", keySetError];
} else {
DDLogError(@"Succesfully set new DB password. First launch?");
DDLogError(@"Succesfully set new DB password.");
}
return newDBPassword;
}
#pragma mark convenience methods
#pragma mark - convenience methods
- (void)purgeCollection:(NSString *)collection {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
@ -378,21 +430,30 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
[TSAttachmentStream deleteAttachments];
}
- (void)wipeSignalStorage {
self.database = nil;
NSError *error;
- (void)deletePasswordFromKeychain
{
[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) {
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

Loading…
Cancel
Save