2017-12-18 23:38:51 +01:00
|
|
|
//
|
2018-01-03 22:19:27 +01:00
|
|
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
2017-12-18 23:38:51 +01:00
|
|
|
//
|
|
|
|
|
|
|
|
#import "OWSStorage.h"
|
|
|
|
#import "AppContext.h"
|
|
|
|
#import "NSData+Base64.h"
|
2017-12-19 04:56:02 +01:00
|
|
|
#import "NSNotificationCenter+OWS.h"
|
2017-12-19 18:02:58 +01:00
|
|
|
#import "OWSFileSystem.h"
|
2017-12-19 05:00:11 +01:00
|
|
|
#import "OWSStorage+Subclass.h"
|
2017-12-18 23:38:51 +01:00
|
|
|
#import "TSAttachmentStream.h"
|
|
|
|
#import "TSStorageManager.h"
|
|
|
|
#import <Curve25519Kit/Randomness.h>
|
|
|
|
#import <SAMKeychain/SAMKeychain.h>
|
2017-12-19 03:42:50 +01:00
|
|
|
#import <YapDatabase/YapDatabase.h>
|
2018-01-24 23:11:18 +01:00
|
|
|
#import <YapDatabase/YapDatabaseCryptoUtils.h>
|
2017-12-18 23:38:51 +01:00
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
2017-12-19 04:56:02 +01:00
|
|
|
NSString *const StorageIsReadyNotification = @"StorageIsReadyNotification";
|
|
|
|
|
2017-12-18 23:38:51 +01:00
|
|
|
NSString *const OWSStorageExceptionName_DatabasePasswordInaccessibleWhileBackgrounded
|
|
|
|
= @"OWSStorageExceptionName_DatabasePasswordInaccessibleWhileBackgrounded";
|
|
|
|
NSString *const OWSStorageExceptionName_DatabasePasswordUnwritable
|
|
|
|
= @"OWSStorageExceptionName_DatabasePasswordUnwritable";
|
|
|
|
NSString *const OWSStorageExceptionName_NoDatabase = @"OWSStorageExceptionName_NoDatabase";
|
2018-01-03 22:19:27 +01:00
|
|
|
NSString *const OWSResetStorageNotification = @"OWSResetStorageNotification";
|
2017-12-18 23:38:51 +01:00
|
|
|
|
|
|
|
static NSString *keychainService = @"TSKeyChainService";
|
|
|
|
static NSString *keychainDBPassAccount = @"TSDatabasePass";
|
2018-01-19 23:27:13 +01:00
|
|
|
static NSString *keychainDBSalt = @"OWSDatabaseSalt";
|
2018-01-24 22:05:28 +01:00
|
|
|
static NSString *keychainDBKeySpec = @"OWSDatabaseKeySpec";
|
2017-12-18 23:38:51 +01:00
|
|
|
|
2018-01-24 16:41:07 +01:00
|
|
|
const NSUInteger kDatabasePasswordLength = 30;
|
|
|
|
|
|
|
|
typedef NSData *_Nullable (^LoadDatabaseMetadataBlock)(NSError **_Nullable);
|
|
|
|
typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void);
|
|
|
|
|
2017-12-18 23:38:51 +01:00
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
@interface YapDatabaseConnection ()
|
|
|
|
|
|
|
|
- (id)initWithDatabase:(YapDatabase *)database;
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
@implementation OWSDatabaseConnection
|
|
|
|
|
|
|
|
- (id)initWithDatabase:(YapDatabase *)database delegate:(id<OWSDatabaseConnectionDelegate>)delegate
|
|
|
|
{
|
|
|
|
self = [super initWithDatabase:database];
|
|
|
|
|
|
|
|
if (!self) {
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
OWSAssert(delegate);
|
|
|
|
|
|
|
|
_delegate = delegate;
|
|
|
|
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
2017-12-19 17:38:25 +01:00
|
|
|
// Assert that the database is in a ready state (specifically that any sync database
|
|
|
|
// view registrations have completed and any async registrations have been started)
|
|
|
|
// before creating write transactions.
|
2017-12-18 23:38:51 +01:00
|
|
|
//
|
|
|
|
// Creating write transactions before the _sync_ database views are registered
|
|
|
|
// causes YapDatabase to rebuild all of our database views, which is catastrophic.
|
2017-12-19 17:38:25 +01:00
|
|
|
// Specifically, it causes YDB's "view version" checks to fail.
|
2017-12-18 23:38:51 +01:00
|
|
|
- (void)readWriteWithBlock:(void (^)(YapDatabaseReadWriteTransaction *transaction))block
|
|
|
|
{
|
|
|
|
id<OWSDatabaseConnectionDelegate> delegate = self.delegate;
|
|
|
|
OWSAssert(delegate);
|
2018-01-26 21:53:26 +01:00
|
|
|
OWSAssert(delegate.areAllRegistrationsComplete || self.canWriteBeforeStorageReady);
|
2017-12-18 23:38:51 +01:00
|
|
|
|
|
|
|
[super readWriteWithBlock:block];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)asyncReadWriteWithBlock:(void (^)(YapDatabaseReadWriteTransaction *transaction))block
|
|
|
|
{
|
2018-01-26 21:53:26 +01:00
|
|
|
[self asyncReadWriteWithBlock:block completionQueue:NULL completionBlock:NULL];
|
2017-12-18 23:38:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)asyncReadWriteWithBlock:(void (^)(YapDatabaseReadWriteTransaction *transaction))block
|
|
|
|
completionBlock:(nullable dispatch_block_t)completionBlock
|
|
|
|
{
|
2018-01-26 21:53:26 +01:00
|
|
|
[self asyncReadWriteWithBlock:block completionQueue:NULL completionBlock:completionBlock];
|
2017-12-18 23:38:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)asyncReadWriteWithBlock:(void (^)(YapDatabaseReadWriteTransaction *transaction))block
|
|
|
|
completionQueue:(nullable dispatch_queue_t)completionQueue
|
|
|
|
completionBlock:(nullable dispatch_block_t)completionBlock
|
|
|
|
{
|
|
|
|
id<OWSDatabaseConnectionDelegate> delegate = self.delegate;
|
|
|
|
OWSAssert(delegate);
|
2018-01-26 21:53:26 +01:00
|
|
|
OWSAssert(delegate.areAllRegistrationsComplete || self.canWriteBeforeStorageReady);
|
2017-12-18 23:38:51 +01:00
|
|
|
|
|
|
|
[super asyncReadWriteWithBlock:block completionQueue:completionQueue completionBlock:completionBlock];
|
|
|
|
}
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
// This class is only used in DEBUG builds.
|
|
|
|
@interface YapDatabase ()
|
|
|
|
|
|
|
|
- (void)addConnection:(YapDatabaseConnection *)connection;
|
|
|
|
|
2018-01-26 21:53:26 +01:00
|
|
|
- (YapDatabaseConnection *)registrationConnection;
|
|
|
|
|
2017-12-18 23:38:51 +01:00
|
|
|
@end
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
@interface OWSDatabase : YapDatabase
|
|
|
|
|
|
|
|
@property (atomic, weak) id<OWSDatabaseConnectionDelegate> delegate;
|
|
|
|
|
2018-01-26 21:53:26 +01:00
|
|
|
@property (atomic, nullable) YapDatabaseConnection *registrationConnectionCached;
|
|
|
|
|
2017-12-18 23:38:51 +01:00
|
|
|
- (instancetype)init NS_UNAVAILABLE;
|
|
|
|
- (id)initWithPath:(NSString *)inPath
|
2017-12-19 00:05:27 +01:00
|
|
|
serializer:(nullable YapDatabaseSerializer)inSerializer
|
2017-12-18 23:38:51 +01:00
|
|
|
deserializer:(YapDatabaseDeserializer)inDeserializer
|
|
|
|
options:(YapDatabaseOptions *)inOptions
|
|
|
|
delegate:(id<OWSDatabaseConnectionDelegate>)delegate NS_DESIGNATED_INITIALIZER;
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
@implementation OWSDatabase
|
|
|
|
|
|
|
|
- (id)initWithPath:(NSString *)inPath
|
2017-12-19 00:05:27 +01:00
|
|
|
serializer:(nullable YapDatabaseSerializer)inSerializer
|
2017-12-18 23:38:51 +01:00
|
|
|
deserializer:(YapDatabaseDeserializer)inDeserializer
|
|
|
|
options:(YapDatabaseOptions *)inOptions
|
|
|
|
delegate:(id<OWSDatabaseConnectionDelegate>)delegate
|
|
|
|
{
|
|
|
|
self = [super initWithPath:inPath serializer:inSerializer deserializer:inDeserializer options:inOptions];
|
|
|
|
|
|
|
|
if (!self) {
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
OWSAssert(delegate);
|
|
|
|
|
|
|
|
_delegate = delegate;
|
|
|
|
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
// This clobbers the superclass implementation to include asserts which
|
|
|
|
// ensure that the database is in a ready state before creating write transactions.
|
|
|
|
//
|
|
|
|
// See comments in OWSDatabaseConnection.
|
|
|
|
- (YapDatabaseConnection *)newConnection
|
|
|
|
{
|
|
|
|
id<OWSDatabaseConnectionDelegate> delegate = self.delegate;
|
|
|
|
OWSAssert(delegate);
|
|
|
|
|
|
|
|
OWSDatabaseConnection *connection = [[OWSDatabaseConnection alloc] initWithDatabase:self delegate:delegate];
|
|
|
|
[self addConnection:connection];
|
|
|
|
return connection;
|
|
|
|
}
|
|
|
|
|
2018-01-26 21:53:26 +01:00
|
|
|
- (YapDatabaseConnection *)registrationConnection
|
|
|
|
{
|
|
|
|
@synchronized(self)
|
|
|
|
{
|
|
|
|
if (!self.registrationConnectionCached) {
|
|
|
|
YapDatabaseConnection *connection = [super registrationConnection];
|
|
|
|
|
|
|
|
#ifdef DEBUG
|
|
|
|
// Flag the registration connection as such.
|
|
|
|
OWSAssert([connection isKindOfClass:[OWSDatabaseConnection class]]);
|
|
|
|
((OWSDatabaseConnection *)connection).canWriteBeforeStorageReady = YES;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
self.registrationConnectionCached = connection;
|
|
|
|
}
|
|
|
|
return self.registrationConnectionCached;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-18 23:38:51 +01:00
|
|
|
@end
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
@interface OWSUnknownDBObject : NSObject <NSCoding>
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
@implementation OWSUnknownDBObject
|
|
|
|
|
|
|
|
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
|
|
|
|
{
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)encodeWithCoder:(NSCoder *)aCoder
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
@interface OWSUnarchiverDelegate : NSObject <NSKeyedUnarchiverDelegate>
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
@implementation OWSUnarchiverDelegate
|
|
|
|
|
|
|
|
- (nullable Class)unarchiver:(NSKeyedUnarchiver *)unarchiver
|
|
|
|
cannotDecodeObjectOfClassName:(NSString *)name
|
|
|
|
originalClasses:(NSArray<NSString *> *)classNames
|
|
|
|
{
|
|
|
|
DDLogError(@"%@ Could not decode object: %@", self.logTag, name);
|
|
|
|
OWSProdError([OWSAnalyticsEvents storageErrorCouldNotDecodeClass]);
|
|
|
|
return [OWSUnknownDBObject class];
|
|
|
|
}
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
@interface OWSStorage () <OWSDatabaseConnectionDelegate>
|
|
|
|
|
|
|
|
@property (atomic, nullable) YapDatabase *database;
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
@implementation OWSStorage
|
|
|
|
|
|
|
|
- (instancetype)initStorage
|
|
|
|
{
|
|
|
|
self = [super init];
|
|
|
|
|
2017-12-19 03:34:22 +01:00
|
|
|
if (self) {
|
|
|
|
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.
|
2018-01-25 19:16:35 +01:00
|
|
|
OWSFail(@"%@ Could not load database", self.logTag);
|
2017-12-19 03:34:22 +01:00
|
|
|
OWSProdCritical([OWSAnalyticsEvents storageErrorCouldNotLoadDatabase]);
|
2017-12-18 23:38:51 +01:00
|
|
|
|
2017-12-19 18:02:58 +01:00
|
|
|
// Try to reset app by deleting all databases.
|
2017-12-20 17:32:48 +01:00
|
|
|
//
|
|
|
|
// TODO: Possibly clean up all app files.
|
|
|
|
// [OWSStorage deleteDatabaseFiles];
|
2017-12-18 23:38:51 +01:00
|
|
|
|
2017-12-19 03:34:22 +01:00
|
|
|
if (![self tryToLoadDatabase]) {
|
2018-01-25 19:16:35 +01:00
|
|
|
OWSFail(@"%@ Could not load database (second try)", self.logTag);
|
2017-12-19 03:34:22 +01:00
|
|
|
OWSProdCritical([OWSAnalyticsEvents storageErrorCouldNotLoadDatabaseSecondAttempt]);
|
2017-12-18 23:38:51 +01:00
|
|
|
|
2017-12-19 03:34:22 +01:00
|
|
|
// Sleep to give analytics events time to be delivered.
|
|
|
|
[NSThread sleepForTimeInterval:15.0f];
|
2017-12-18 23:38:51 +01:00
|
|
|
|
2018-01-25 16:44:13 +01:00
|
|
|
OWSRaiseException(OWSStorageExceptionName_NoDatabase, @"Failed to initialize database.");
|
2017-12-19 03:34:22 +01:00
|
|
|
}
|
2017-12-18 23:38:51 +01:00
|
|
|
}
|
2018-01-03 22:19:27 +01:00
|
|
|
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
selector:@selector(resetStorage)
|
|
|
|
name:OWSResetStorageNotification
|
|
|
|
object:nil];
|
2017-12-18 23:38:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
2018-01-03 22:19:27 +01:00
|
|
|
- (void)dealloc
|
|
|
|
{
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
|
|
}
|
|
|
|
|
2018-01-11 15:56:38 +01:00
|
|
|
- (nullable id)dbNotificationObject
|
|
|
|
{
|
|
|
|
OWSAssert(self.database);
|
|
|
|
|
|
|
|
return self.database;
|
|
|
|
}
|
|
|
|
|
2017-12-19 04:56:02 +01:00
|
|
|
- (BOOL)areAsyncRegistrationsComplete
|
|
|
|
{
|
|
|
|
OWS_ABSTRACT_METHOD();
|
|
|
|
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL)areSyncRegistrationsComplete
|
|
|
|
{
|
|
|
|
OWS_ABSTRACT_METHOD();
|
|
|
|
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
2018-01-26 21:53:26 +01:00
|
|
|
- (BOOL)areAllRegistrationsComplete
|
|
|
|
{
|
|
|
|
return self.areSyncRegistrationsComplete && self.areAsyncRegistrationsComplete;
|
|
|
|
}
|
|
|
|
|
2017-12-19 04:56:02 +01:00
|
|
|
- (void)runSyncRegistrations
|
|
|
|
{
|
|
|
|
OWS_ABSTRACT_METHOD();
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)runAsyncRegistrationsWithCompletion:(void (^_Nonnull)(void))completion
|
2017-12-18 23:38:51 +01:00
|
|
|
{
|
2017-12-19 04:56:02 +01:00
|
|
|
OWS_ABSTRACT_METHOD();
|
|
|
|
}
|
2017-12-18 23:38:51 +01:00
|
|
|
|
2017-12-19 04:56:02 +01:00
|
|
|
+ (NSArray<OWSStorage *> *)allStorages
|
|
|
|
{
|
|
|
|
return @[
|
|
|
|
TSStorageManager.sharedManager,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2018-01-26 22:51:01 +01:00
|
|
|
+ (void)setupStorage
|
2017-12-19 04:56:02 +01:00
|
|
|
{
|
|
|
|
for (OWSStorage *storage in self.allStorages) {
|
|
|
|
[storage runSyncRegistrations];
|
|
|
|
}
|
|
|
|
|
|
|
|
for (OWSStorage *storage in self.allStorages) {
|
|
|
|
[storage runAsyncRegistrationsWithCompletion:^{
|
2018-01-26 21:53:26 +01:00
|
|
|
|
2017-12-19 04:56:02 +01:00
|
|
|
[self postRegistrationCompleteNotificationIfPossible];
|
2018-01-26 21:53:26 +01:00
|
|
|
|
2018-01-26 22:51:01 +01:00
|
|
|
((OWSDatabase *)storage.database).registrationConnectionCached = nil;
|
|
|
|
}];
|
2017-12-19 04:56:02 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-26 21:53:26 +01:00
|
|
|
- (YapDatabaseConnection *)registrationConnection
|
|
|
|
{
|
|
|
|
return self.database.registrationConnection;
|
|
|
|
}
|
|
|
|
|
2017-12-19 04:56:02 +01:00
|
|
|
+ (void)postRegistrationCompleteNotificationIfPossible
|
|
|
|
{
|
|
|
|
if (!self.isStorageReady) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
dispatch_once(&onceToken, ^{
|
|
|
|
[[NSNotificationCenter defaultCenter] postNotificationNameAsync:StorageIsReadyNotification
|
|
|
|
object:nil
|
|
|
|
userInfo:nil];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (BOOL)isStorageReady
|
|
|
|
{
|
|
|
|
for (OWSStorage *storage in self.allStorages) {
|
2018-01-26 21:53:26 +01:00
|
|
|
if (!storage.areAllRegistrationsComplete) {
|
2017-12-19 04:56:02 +01:00
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return YES;
|
2017-12-18 23:38:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL)tryToLoadDatabase
|
|
|
|
{
|
2018-01-24 23:11:18 +01:00
|
|
|
// We determine the database password, salt and key spec first, since a side effect of
|
2017-12-18 23:38:51 +01:00
|
|
|
// this can be deleting any existing database file (if we're recovering
|
|
|
|
// from a corrupt keychain).
|
2018-01-25 16:44:13 +01:00
|
|
|
//
|
|
|
|
// Although we don't use databasePassword or databaseSalt in this method,
|
|
|
|
// we use their accessors to ensure that all three exist in the keychain
|
|
|
|
// and can be loaded or that we reset the database & keychain.
|
2018-01-24 23:11:18 +01:00
|
|
|
NSData *databasePassword = [self databasePassword];
|
|
|
|
OWSAssert(databasePassword.length > 0);
|
|
|
|
NSData *databaseSalt = [self databaseSalt];
|
|
|
|
OWSAssert(databaseSalt.length > 0);
|
2018-01-24 22:05:28 +01:00
|
|
|
NSData *databaseKeySpec = [self databaseKeySpec];
|
|
|
|
OWSAssert(databaseKeySpec.length == kSQLCipherKeySpecLength);
|
2017-12-18 23:38:51 +01:00
|
|
|
|
|
|
|
YapDatabaseOptions *options = [[YapDatabaseOptions alloc] init];
|
|
|
|
options.corruptAction = YapDatabaseCorruptAction_Fail;
|
|
|
|
options.enableMultiProcessSupport = YES;
|
2018-01-24 22:05:28 +01:00
|
|
|
options.cipherKeySpecBlock = ^{
|
|
|
|
return databaseKeySpec;
|
2018-01-24 16:41:07 +01:00
|
|
|
};
|
|
|
|
options.cipherUnencryptedHeaderLength = kSqliteHeaderLength;
|
2017-12-18 23:38:51 +01:00
|
|
|
|
2018-01-19 17:34:59 +01:00
|
|
|
// If any of these asserts fails, we need to verify and update
|
|
|
|
// OWSDatabaseConverter which assumes the values of these options.
|
|
|
|
OWSAssert(options.cipherDefaultkdfIterNumber == 0);
|
|
|
|
OWSAssert(options.kdfIterNumber == 0);
|
|
|
|
OWSAssert(options.cipherPageSize == 0);
|
|
|
|
OWSAssert(options.pragmaPageSize == 0);
|
|
|
|
OWSAssert(options.pragmaJournalSizeLimit == 0);
|
|
|
|
OWSAssert(options.pragmaMMapSize == 0);
|
|
|
|
|
2017-12-19 18:02:58 +01:00
|
|
|
OWSDatabase *database = [[OWSDatabase alloc] initWithPath:[self databaseFilePath]
|
2017-12-19 00:05:27 +01:00
|
|
|
serializer:nil
|
2017-12-18 23:38:51 +01:00
|
|
|
deserializer:[[self class] logOnFailureDeserializer]
|
|
|
|
options:options
|
|
|
|
delegate:self];
|
|
|
|
|
|
|
|
if (!database) {
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
|
|
|
_database = database;
|
|
|
|
|
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* NSCoding sometimes throws exceptions killing our app. We want to log that exception.
|
|
|
|
**/
|
|
|
|
+ (YapDatabaseDeserializer)logOnFailureDeserializer
|
|
|
|
{
|
|
|
|
OWSUnarchiverDelegate *unarchiverDelegate = [OWSUnarchiverDelegate new];
|
|
|
|
|
|
|
|
return ^id(NSString __unused *collection, NSString __unused *key, NSData *data) {
|
|
|
|
if (!data || data.length <= 0) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
@try {
|
|
|
|
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
|
|
|
|
unarchiver.delegate = unarchiverDelegate;
|
|
|
|
return [unarchiver decodeObjectForKey:@"root"];
|
|
|
|
} @catch (NSException *exception) {
|
|
|
|
// Sync log in case we bail.
|
|
|
|
OWSProdError([OWSAnalyticsEvents storageErrorDeserialization]);
|
|
|
|
@throw exception;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
- (nullable YapDatabaseConnection *)newDatabaseConnection
|
|
|
|
{
|
|
|
|
return self.database.newConnection;
|
|
|
|
}
|
|
|
|
|
2018-01-26 21:53:26 +01:00
|
|
|
#ifdef DEBUG
|
2017-12-19 00:05:27 +01:00
|
|
|
- (BOOL)registerExtension:(YapDatabaseExtension *)extension withName:(NSString *)extensionName
|
|
|
|
{
|
|
|
|
return [self.database registerExtension:extension withName:extensionName];
|
|
|
|
}
|
2018-01-26 21:53:26 +01:00
|
|
|
#endif
|
2017-12-19 00:05:27 +01:00
|
|
|
|
|
|
|
- (void)asyncRegisterExtension:(YapDatabaseExtension *)extension
|
|
|
|
withName:(NSString *)extensionName
|
|
|
|
{
|
2018-01-26 21:53:26 +01:00
|
|
|
[self.database asyncRegisterExtension:extension
|
|
|
|
withName:extensionName
|
|
|
|
completionBlock:^(BOOL ready) {
|
|
|
|
if (!ready) {
|
|
|
|
OWSFail(@"%@ asyncRegisterExtension failed: %@", self.logTag, extensionName);
|
|
|
|
} else {
|
|
|
|
DDLogVerbose(@"%@ asyncRegisterExtension succeeded: %@", self.logTag, extensionName);
|
|
|
|
}
|
|
|
|
}];
|
2017-12-19 00:05:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
- (nullable id)registeredExtension:(NSString *)extensionName
|
|
|
|
{
|
|
|
|
return [self.database registeredExtension:extensionName];
|
|
|
|
}
|
|
|
|
|
2017-12-18 23:38:51 +01:00
|
|
|
#pragma mark - Password
|
|
|
|
|
2017-12-19 18:02:58 +01:00
|
|
|
+ (void)deleteDatabaseFiles
|
|
|
|
{
|
|
|
|
[OWSFileSystem deleteFile:[TSStorageManager databaseFilePath]];
|
|
|
|
}
|
|
|
|
|
2017-12-18 23:38:51 +01:00
|
|
|
- (void)deleteDatabaseFile
|
|
|
|
{
|
2017-12-19 18:02:58 +01:00
|
|
|
[OWSFileSystem deleteFile:[self databaseFilePath]];
|
2017-12-18 23:38:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)resetStorage
|
|
|
|
{
|
|
|
|
self.database = nil;
|
|
|
|
|
|
|
|
[self deleteDatabaseFile];
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (void)resetAllStorage
|
|
|
|
{
|
2018-01-03 22:19:27 +01:00
|
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:OWSResetStorageNotification object:nil];
|
2017-12-18 23:38:51 +01:00
|
|
|
|
2017-12-19 18:02:58 +01:00
|
|
|
// This might be redundant but in the spirit of thoroughness...
|
|
|
|
[self deleteDatabaseFiles];
|
|
|
|
|
2017-12-18 23:38:51 +01:00
|
|
|
[self deletePasswordFromKeychain];
|
|
|
|
|
|
|
|
if (CurrentAppContext().isMainApp) {
|
|
|
|
[TSAttachmentStream deleteAttachments];
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Delete Profiles on Disk?
|
|
|
|
}
|
|
|
|
|
|
|
|
#pragma mark - Password
|
|
|
|
|
2017-12-19 18:02:58 +01:00
|
|
|
- (NSString *)databaseFilePath
|
2017-12-18 23:38:51 +01:00
|
|
|
{
|
|
|
|
OWS_ABSTRACT_METHOD();
|
|
|
|
|
|
|
|
return @"";
|
|
|
|
}
|
|
|
|
|
2018-01-19 23:27:13 +01:00
|
|
|
#pragma mark - Keychain
|
2017-12-18 23:38:51 +01:00
|
|
|
|
|
|
|
+ (BOOL)isDatabasePasswordAccessible
|
|
|
|
{
|
|
|
|
[SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
|
|
|
|
NSError *error;
|
|
|
|
NSString *dbPassword = [SAMKeychain passwordForService:keychainService account:keychainDBPassAccount error:&error];
|
|
|
|
|
|
|
|
if (dbPassword && !error) {
|
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
DDLogWarn(@"Database password couldn't be accessed: %@", error.localizedDescription);
|
|
|
|
}
|
|
|
|
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
2018-01-19 23:27:13 +01:00
|
|
|
+ (nullable NSData *)tryToLoadKeyChainValue:(NSString *)keychainKey errorHandle:(NSError **)errorHandle
|
2017-12-18 23:38:51 +01:00
|
|
|
{
|
2018-01-19 23:27:13 +01:00
|
|
|
OWSAssert(keychainKey.length > 0);
|
2018-01-19 15:59:17 +01:00
|
|
|
OWSAssert(errorHandle);
|
|
|
|
|
2017-12-18 23:38:51 +01:00
|
|
|
[SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
|
|
|
|
|
2018-01-19 23:27:13 +01:00
|
|
|
return [SAMKeychain passwordDataForService:keychainService account:keychainKey error:errorHandle];
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (nullable NSData *)tryToLoadDatabasePassword:(NSError **)errorHandle
|
|
|
|
{
|
|
|
|
return [self tryToLoadKeyChainValue:keychainDBPassAccount errorHandle:errorHandle];
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (nullable NSData *)tryToLoadDatabaseSalt:(NSError **)errorHandle
|
|
|
|
{
|
|
|
|
return [self tryToLoadKeyChainValue:keychainDBSalt errorHandle:errorHandle];
|
2018-01-19 15:59:17 +01:00
|
|
|
}
|
|
|
|
|
2018-01-24 22:05:28 +01:00
|
|
|
+ (nullable NSData *)tryToLoadDatabaseKeySpec:(NSError **)errorHandle
|
|
|
|
{
|
|
|
|
return [self tryToLoadKeyChainValue:keychainDBKeySpec errorHandle:errorHandle];
|
|
|
|
}
|
|
|
|
|
2018-01-19 15:59:17 +01:00
|
|
|
- (NSData *)databasePassword
|
|
|
|
{
|
2018-01-24 16:41:07 +01:00
|
|
|
return [self loadMetadataOrClearDatabase:^(NSError **_Nullable errorHandle) {
|
|
|
|
return [OWSStorage tryToLoadDatabasePassword:errorHandle];
|
|
|
|
}
|
|
|
|
createDataBlock:^{
|
2018-01-24 23:11:18 +01:00
|
|
|
NSData *passwordData = [self createAndSetNewDatabasePassword];
|
|
|
|
NSData *saltData = [self createAndSetNewDatabaseSalt];
|
|
|
|
NSData *keySpecData = [self createAndSetNewDatabaseKeySpec];
|
|
|
|
|
|
|
|
OWSAssert(passwordData.length > 0);
|
|
|
|
OWSAssert(saltData.length == kSQLCipherSaltLength);
|
|
|
|
OWSAssert(keySpecData.length == kSQLCipherKeySpecLength);
|
|
|
|
|
|
|
|
return passwordData;
|
2018-01-24 16:41:07 +01:00
|
|
|
}
|
|
|
|
label:@"Database password"];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSData *)databaseSalt
|
|
|
|
{
|
|
|
|
return [self loadMetadataOrClearDatabase:^(NSError **_Nullable errorHandle) {
|
|
|
|
return [OWSStorage tryToLoadDatabaseSalt:errorHandle];
|
|
|
|
}
|
|
|
|
createDataBlock:^{
|
2018-01-24 23:11:18 +01:00
|
|
|
NSData *passwordData = [self createAndSetNewDatabasePassword];
|
|
|
|
NSData *saltData = [self createAndSetNewDatabaseSalt];
|
|
|
|
NSData *keySpecData = [self createAndSetNewDatabaseKeySpec];
|
|
|
|
|
|
|
|
OWSAssert(passwordData.length > 0);
|
|
|
|
OWSAssert(saltData.length == kSQLCipherSaltLength);
|
|
|
|
OWSAssert(keySpecData.length == kSQLCipherKeySpecLength);
|
|
|
|
|
|
|
|
return saltData;
|
2018-01-24 16:41:07 +01:00
|
|
|
}
|
|
|
|
label:@"Database salt"];
|
|
|
|
}
|
|
|
|
|
2018-01-24 22:05:28 +01:00
|
|
|
- (NSData *)databaseKeySpec
|
|
|
|
{
|
|
|
|
return [self loadMetadataOrClearDatabase:^(NSError **_Nullable errorHandle) {
|
|
|
|
return [OWSStorage tryToLoadDatabaseKeySpec:errorHandle];
|
|
|
|
}
|
|
|
|
createDataBlock:^{
|
2018-01-24 23:11:18 +01:00
|
|
|
OWSFail(@"%@ It should never be necessary to generate a random key spec.", self.logTag);
|
|
|
|
|
|
|
|
NSData *passwordData = [self createAndSetNewDatabasePassword];
|
|
|
|
NSData *saltData = [self createAndSetNewDatabaseSalt];
|
|
|
|
NSData *keySpecData = [self createAndSetNewDatabaseKeySpec];
|
|
|
|
|
|
|
|
OWSAssert(passwordData.length > 0);
|
|
|
|
OWSAssert(saltData.length == kSQLCipherSaltLength);
|
|
|
|
OWSAssert(keySpecData.length == kSQLCipherKeySpecLength);
|
|
|
|
|
|
|
|
return keySpecData;
|
2018-01-24 22:05:28 +01:00
|
|
|
}
|
|
|
|
label:@"Database key spec"];
|
|
|
|
}
|
|
|
|
|
2018-01-24 16:41:07 +01:00
|
|
|
- (NSData *)loadMetadataOrClearDatabase:(LoadDatabaseMetadataBlock)loadDataBlock
|
|
|
|
createDataBlock:(CreateDatabaseMetadataBlock)createDataBlock
|
|
|
|
label:(NSString *)label
|
|
|
|
{
|
|
|
|
OWSAssert(loadDataBlock);
|
|
|
|
OWSAssert(createDataBlock);
|
|
|
|
|
|
|
|
NSError *error;
|
|
|
|
NSData *_Nullable data = loadDataBlock(&error);
|
2017-12-18 23:38:51 +01:00
|
|
|
|
2018-01-24 16:41:07 +01:00
|
|
|
if (error) {
|
|
|
|
// Because we use kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
|
|
|
|
// the keychain will be inaccessible after device restart until
|
|
|
|
// device is unlocked for the first time. If the app receives
|
|
|
|
// a push notification, we won't be able to access the keychain to
|
|
|
|
// process that notification, so we should just terminate by throwing
|
|
|
|
// an uncaught exception.
|
2017-12-18 23:38:51 +01:00
|
|
|
NSString *errorDescription =
|
2018-01-24 16:41:07 +01:00
|
|
|
[NSString stringWithFormat:@"%@ inaccessible. No unlock since device restart? Error: %@", label, error];
|
2017-12-18 23:38:51 +01:00
|
|
|
if (CurrentAppContext().isMainApp) {
|
|
|
|
UIApplicationState applicationState = CurrentAppContext().mainApplicationState;
|
|
|
|
errorDescription =
|
|
|
|
[errorDescription stringByAppendingFormat:@", ApplicationState: %d", (int)applicationState];
|
|
|
|
}
|
|
|
|
DDLogError(@"%@ %@", self.logTag, errorDescription);
|
|
|
|
[DDLog flushLog];
|
|
|
|
|
|
|
|
if (CurrentAppContext().isMainApp) {
|
2018-01-12 20:24:35 +01:00
|
|
|
if (CurrentAppContext().isInBackground) {
|
2017-12-18 23:38:51 +01:00
|
|
|
// TODO: Rather than crash here, we should detect the situation earlier
|
|
|
|
// and exit gracefully - (in the app delegate?). See the `
|
|
|
|
// This is a last ditch effort to avoid blowing away the user's database.
|
|
|
|
[self backgroundedAppDatabasePasswordInaccessibleWithErrorDescription:errorDescription];
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
[self backgroundedAppDatabasePasswordInaccessibleWithErrorDescription:
|
2018-01-24 16:41:07 +01:00
|
|
|
[NSString stringWithFormat:@"%@ inaccessible; not main app.", label]];
|
2017-12-18 23:38:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// At this point, 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.
|
|
|
|
|
2018-01-24 16:41:07 +01:00
|
|
|
BOOL shouldHaveDatabaseMetadata = [NSFileManager.defaultManager fileExistsAtPath:[self databaseFilePath]];
|
|
|
|
if (shouldHaveDatabaseMetadata) {
|
2018-01-25 19:16:35 +01:00
|
|
|
OWSFail(@"%@ Could not load database metadata", self.logTag);
|
2017-12-18 23:38:51 +01:00
|
|
|
OWSProdCritical([OWSAnalyticsEvents storageErrorCouldNotLoadDatabaseSecondAttempt]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try to reset app by deleting database.
|
|
|
|
[OWSStorage resetAllStorage];
|
|
|
|
|
2018-01-24 16:41:07 +01:00
|
|
|
data = createDataBlock();
|
2017-12-18 23:38:51 +01:00
|
|
|
}
|
|
|
|
|
2018-01-24 16:41:07 +01:00
|
|
|
return data;
|
2017-12-18 23:38:51 +01:00
|
|
|
}
|
|
|
|
|
2018-01-19 15:59:17 +01:00
|
|
|
- (NSData *)createAndSetNewDatabasePassword
|
2017-12-18 23:38:51 +01:00
|
|
|
{
|
2018-01-24 16:41:07 +01:00
|
|
|
NSData *password = [[[Randomness generateRandomBytes:kDatabasePasswordLength] base64EncodedString]
|
|
|
|
dataUsingEncoding:NSUTF8StringEncoding];
|
2017-12-18 23:38:51 +01:00
|
|
|
|
2018-01-09 16:12:14 +01:00
|
|
|
[OWSStorage storeDatabasePassword:password];
|
2017-12-18 23:38:51 +01:00
|
|
|
|
2018-01-09 16:12:14 +01:00
|
|
|
return password;
|
2017-12-18 23:38:51 +01:00
|
|
|
}
|
|
|
|
|
2018-01-24 16:41:07 +01:00
|
|
|
- (NSData *)createAndSetNewDatabaseSalt
|
|
|
|
{
|
2018-01-24 23:11:18 +01:00
|
|
|
NSData *saltData = [Randomness generateRandomBytes:(int)kSQLCipherSaltLength];
|
2018-01-24 16:41:07 +01:00
|
|
|
|
|
|
|
[OWSStorage storeDatabaseSalt:saltData];
|
|
|
|
|
|
|
|
return saltData;
|
|
|
|
}
|
|
|
|
|
2018-01-24 22:05:28 +01:00
|
|
|
- (NSData *)createAndSetNewDatabaseKeySpec
|
|
|
|
{
|
|
|
|
NSData *databasePassword = [self databasePassword];
|
|
|
|
OWSAssert(databasePassword.length > 0);
|
|
|
|
NSData *databaseSalt = [self databaseSalt];
|
|
|
|
OWSAssert(databaseSalt.length == kSQLCipherSaltLength);
|
|
|
|
|
2018-01-24 23:11:18 +01:00
|
|
|
NSData *keySpecData = [YapDatabaseCryptoUtils databaseKeySpecForPassword:databasePassword saltData:databaseSalt];
|
2018-01-24 22:05:28 +01:00
|
|
|
OWSAssert(keySpecData.length == kSQLCipherKeySpecLength);
|
|
|
|
|
|
|
|
[OWSStorage storeDatabaseKeySpec:keySpecData];
|
|
|
|
|
|
|
|
return keySpecData;
|
|
|
|
}
|
|
|
|
|
2017-12-18 23:38:51 +01:00
|
|
|
- (void)backgroundedAppDatabasePasswordInaccessibleWithErrorDescription:(NSString *)errorDescription
|
|
|
|
{
|
2018-01-12 20:24:35 +01:00
|
|
|
OWSAssert(CurrentAppContext().isMainApp && CurrentAppContext().isInBackground);
|
2017-12-18 23:38:51 +01:00
|
|
|
|
|
|
|
// Sleep to give analytics events time to be delivered.
|
|
|
|
[NSThread sleepForTimeInterval:5.0f];
|
|
|
|
|
|
|
|
// Presumably this happened in response to a push notification. It's possible that the keychain is corrupted
|
|
|
|
// but it could also just be that the user hasn't yet unlocked their device since our password is
|
|
|
|
// kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
2018-01-25 16:44:13 +01:00
|
|
|
OWSRaiseException(OWSStorageExceptionName_DatabasePasswordInaccessibleWhileBackgrounded, @"%@", errorDescription);
|
2017-12-18 23:38:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
+ (void)deletePasswordFromKeychain
|
|
|
|
{
|
|
|
|
[SAMKeychain deletePasswordForService:keychainService account:keychainDBPassAccount];
|
2018-01-19 23:27:13 +01:00
|
|
|
[SAMKeychain deletePasswordForService:keychainService account:keychainDBSalt];
|
2018-01-24 23:11:18 +01:00
|
|
|
[SAMKeychain deletePasswordForService:keychainService account:keychainDBKeySpec];
|
2017-12-18 23:38:51 +01:00
|
|
|
}
|
|
|
|
|
2018-01-10 17:14:02 +01:00
|
|
|
- (unsigned long long)databaseFileSize
|
|
|
|
{
|
|
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
|
|
NSError *_Nullable error;
|
|
|
|
unsigned long long fileSize =
|
|
|
|
[[fileManager attributesOfItemAtPath:self.databaseFilePath error:&error][NSFileSize] unsignedLongLongValue];
|
|
|
|
if (error) {
|
|
|
|
DDLogError(@"%@ Couldn't fetch database file size: %@", self.logTag, error);
|
|
|
|
} else {
|
|
|
|
DDLogInfo(@"%@ Database file size: %llu", self.logTag, fileSize);
|
|
|
|
}
|
|
|
|
return fileSize;
|
|
|
|
}
|
|
|
|
|
2018-01-19 23:27:13 +01:00
|
|
|
+ (void)storeKeyChainValue:(NSData *)data keychainKey:(NSString *)keychainKey
|
2018-01-09 16:12:14 +01:00
|
|
|
{
|
2018-01-19 23:27:13 +01:00
|
|
|
OWSAssert(keychainKey.length > 0);
|
|
|
|
OWSAssert(data.length > 0);
|
|
|
|
|
2018-01-09 16:12:14 +01:00
|
|
|
NSError *error;
|
|
|
|
[SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
|
2018-01-19 23:27:13 +01:00
|
|
|
BOOL success = [SAMKeychain setPasswordData:data forService:keychainService account:keychainKey error:&error];
|
2018-01-09 16:12:14 +01:00
|
|
|
if (!success || error) {
|
2018-01-25 19:16:35 +01:00
|
|
|
OWSFail(@"%@ Could not store database metadata", self.logTag);
|
2018-01-19 23:27:13 +01:00
|
|
|
OWSProdCritical([OWSAnalyticsEvents storageErrorCouldNotStoreKeychainValue]);
|
2018-01-09 16:12:14 +01:00
|
|
|
|
|
|
|
[OWSStorage deletePasswordFromKeychain];
|
|
|
|
|
|
|
|
// Sleep to give analytics events time to be delivered.
|
|
|
|
[NSThread sleepForTimeInterval:15.0f];
|
|
|
|
|
2018-01-25 16:44:13 +01:00
|
|
|
OWSRaiseException(
|
|
|
|
OWSStorageExceptionName_DatabasePasswordUnwritable, @"Setting keychain value failed with error: %@", error);
|
2018-01-09 16:12:14 +01:00
|
|
|
} else {
|
2018-01-19 23:27:13 +01:00
|
|
|
DDLogWarn(@"Succesfully set new keychain value.");
|
2018-01-09 16:12:14 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-19 23:27:13 +01:00
|
|
|
+ (void)storeDatabasePassword:(NSData *)passwordData
|
|
|
|
{
|
|
|
|
[self storeKeyChainValue:passwordData keychainKey:keychainDBPassAccount];
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (void)storeDatabaseSalt:(NSData *)saltData
|
|
|
|
{
|
2018-01-24 16:41:07 +01:00
|
|
|
OWSAssert(saltData.length == kSQLCipherSaltLength);
|
|
|
|
|
2018-01-19 23:27:13 +01:00
|
|
|
[self storeKeyChainValue:saltData keychainKey:keychainDBSalt];
|
|
|
|
}
|
|
|
|
|
2018-01-24 22:05:28 +01:00
|
|
|
+ (void)storeDatabaseKeySpec:(NSData *)keySpecData
|
|
|
|
{
|
|
|
|
OWSAssert(keySpecData.length == kSQLCipherKeySpecLength);
|
|
|
|
|
|
|
|
[self storeKeyChainValue:keySpecData keychainKey:keychainDBKeySpec];
|
|
|
|
}
|
|
|
|
|
2017-12-18 23:38:51 +01:00
|
|
|
@end
|
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_END
|