session-ios/src/Storage/TSStorageManager.m

374 lines
12 KiB
Mathematica
Raw Normal View History

2015-12-07 03:31:43 +01:00
// Created by Frederic Jacobs on 27/10/14.
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
#import "TSStorageManager.h"
2015-12-07 03:31:43 +01:00
#import "NSData+Base64.h"
#import "OWSReadReceipt.h"
#import "SignalRecipient.h"
2015-12-07 03:31:43 +01:00
#import "TSAttachmentStream.h"
#import "TSDatabaseSecondaryIndexes.h"
#import "TSDatabaseView.h"
#import "TSInteraction.h"
#import "TSThread.h"
#import <25519/Randomness.h>
#import <SAMKeychain/SAMKeychain.h>
#import <YapDatabase/YapDatabaseRelationship.h>
2015-12-07 03:31:43 +01:00
NSString *const TSUIDatabaseConnectionDidUpdateNotification = @"TSUIDatabaseConnectionDidUpdateNotification";
static const NSString *const databaseName = @"Signal.sqlite";
static NSString *keychainService = @"TSKeyChainService";
static NSString *keychainDBPassAccount = @"TSDatabasePass";
@interface TSStorageManager ()
@property YapDatabase *database;
@end
// Some lingering TSRecipient records in the wild causing crashes.
// This is a stop gap until a proper cleanup happens.
@interface TSRecipient : NSObject <NSCoding>
@end
@interface OWSUnknownObject : NSObject <NSCoding>
@end
/**
* A default object to returned 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 OWSUnknownObject
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
return nil;
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
}
@end
@interface OWSUnarchiverDelegate : NSObject <NSKeyedUnarchiverDelegate>
@end
@implementation OWSUnarchiverDelegate
- (nullable Class)unarchiver:(NSKeyedUnarchiver *)unarchiver cannotDecodeObjectOfClassName:(NSString *)name originalClasses:(NSArray<NSString *> *)classNames
{
DDLogError(@"[OWSUnarchiverDelegate] Ignoring unknown class name: %@. Was the class definition deleted?", name);
return [OWSUnknownObject class];
}
@end
2015-12-07 03:31:43 +01:00
@implementation TSStorageManager
+ (instancetype)sharedManager {
static TSStorageManager *sharedMyManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedMyManager = [[self alloc] init];
#if TARGET_OS_IPHONE
[sharedMyManager protectSignalFiles];
#endif
});
return sharedMyManager;
}
- (instancetype)init {
self = [super init];
YapDatabaseOptions *options = [[YapDatabaseOptions alloc] init];
options.corruptAction = YapDatabaseCorruptAction_Fail;
options.cipherKeyBlock = ^{
return [self databasePassword];
};
_database = [[YapDatabase alloc] initWithPath:[self dbPath]
serializer:NULL
deserializer:[[self class] logOnFailureDeserializer]
options:options];
2015-12-07 03:31:43 +01:00
_dbConnection = self.newDatabaseConnection;
return self;
}
/**
* 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.
DDLogError(@"%@ Unarchiving key:%@ from collection:%@ and data %@ failed with error: %@",
self.tag,
key,
collection,
data,
exception.reason);
DDLogError(@"%@ Raising exception.", self.tag);
@throw exception;
}
};
}
- (void)setupDatabase
{
// Register extensions which are essential for rendering threads synchronously
2015-12-07 03:31:43 +01:00
[TSDatabaseView registerThreadDatabaseView];
[TSDatabaseView registerBuddyConversationDatabaseView];
[TSDatabaseView registerUnreadDatabaseView];
[self.database registerExtension:[TSDatabaseSecondaryIndexes registerTimeStampIndex] withName:@"idx"];
// Register extensions which aren't essential for rendering threads async
[TSDatabaseView asyncRegisterSecondaryDevicesDatabaseView];
[OWSReadReceipt asyncRegisterIndexOnSenderIdAndTimestampWithDatabase:self.database];
2015-12-07 03:31:43 +01:00
}
- (void)protectSignalFiles {
[self protectFolderAtPath:[TSAttachmentStream attachmentsFolder]];
[self protectFolderAtPath:[self dbPath]];
[self protectFolderAtPath:[[self dbPath] stringByAppendingString:@"-shm"]];
[self protectFolderAtPath:[[self dbPath] stringByAppendingString:@"-wal"]];
}
- (void)protectFolderAtPath:(NSString *)path {
if (![NSFileManager.defaultManager fileExistsAtPath:path]) {
return;
}
NSError *error;
NSDictionary *fileProtection = @{NSFileProtectionKey : NSFileProtectionCompleteUntilFirstUserAuthentication};
[[NSFileManager defaultManager] setAttributes:fileProtection ofItemAtPath:path error:&error];
NSDictionary *resourcesAttrs = @{ NSURLIsExcludedFromBackupKey : @YES };
NSURL *ressourceURL = [NSURL fileURLWithPath:path];
BOOL success = [ressourceURL setResourceValues:resourcesAttrs error:&error];
if (error || !success) {
DDLogError(@"Error while removing files from backup: %@", error.description);
return;
}
}
- (YapDatabaseConnection *)newDatabaseConnection {
return self.database.newConnection;
}
- (BOOL)userSetPassword {
return FALSE;
}
- (BOOL)dbExists {
return [[NSFileManager defaultManager] fileExistsAtPath:[self dbPath]];
}
- (NSString *)dbPath {
NSString *databasePath;
NSFileManager *fileManager = [NSFileManager defaultManager];
#if TARGET_OS_IPHONE
NSURL *fileURL = [[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
NSString *path = [fileURL path];
databasePath = [path stringByAppendingFormat:@"/%@", databaseName];
#elif TARGET_OS_MAC
NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier];
NSArray *urlPaths = [fileManager URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask];
NSURL *appDirectory = [[urlPaths objectAtIndex:0] URLByAppendingPathComponent:bundleID isDirectory:YES];
if (![fileManager fileExistsAtPath:[appDirectory path]]) {
[fileManager createDirectoryAtURL:appDirectory withIntermediateDirectories:NO attributes:nil error:nil];
}
databasePath = [appDirectory.filePathURL.absoluteString stringByAppendingFormat:@"/%@", databaseName];
#endif
return databasePath;
}
- (BOOL)databasePasswordAccessible
{
[SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
2015-12-07 03:31:43 +01:00
NSError *error;
NSString *dbPassword = [SAMKeychain passwordForService:keychainService account:keychainDBPassAccount error:&error];
2015-12-07 03:31:43 +01:00
if (dbPassword && !error) {
return YES;
}
if (error) {
DDLogWarn(@"Database password couldn't be accessed: %@", error.localizedDescription);
}
return NO;
}
- (NSData *)databasePassword
{
[SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
NSString *dbPassword = [SAMKeychain passwordForService:keychainService account:keychainDBPassAccount];
2015-12-07 03:31:43 +01:00
if (!dbPassword) {
dbPassword = [[Randomness generateRandomBytes:30] base64EncodedString];
NSError *error;
[SAMKeychain setPassword:dbPassword forService:keychainService account:keychainDBPassAccount error:&error];
if (error) {
// Sync log to ensure it logs before exiting
NSLog(@"Exiting because we failed to set new DB password. error: %@", error);
exit(1);
} else {
DDLogError(@"Succesfully set new DB password. First launch?");
}
2015-12-07 03:31:43 +01:00
}
return [dbPassword dataUsingEncoding:NSUTF8StringEncoding];
}
#pragma mark convenience methods
- (void)purgeCollection:(NSString *)collection {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[transaction removeAllObjectsInCollection:collection];
}];
}
- (void)setObject:(id)object forKey:(NSString *)key inCollection:(NSString *)collection {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[transaction setObject:object forKey:key inCollection:collection];
}];
}
- (void)removeObjectForKey:(NSString *)string inCollection:(NSString *)collection {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[transaction removeObjectForKey:string inCollection:collection];
}];
}
- (id)objectForKey:(NSString *)key inCollection:(NSString *)collection {
__block NSString *object;
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
object = [transaction objectForKey:key inCollection:collection];
}];
return object;
}
- (NSDictionary *)dictionaryForKey:(NSString *)key inCollection:(NSString *)collection {
__block NSDictionary *object;
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
object = [transaction objectForKey:key inCollection:collection];
}];
return object;
}
- (NSString *)stringForKey:(NSString *)key inCollection:(NSString *)collection {
NSString *string = [self objectForKey:key inCollection:collection];
return string;
}
- (BOOL)boolForKey:(NSString *)key inCollection:(NSString *)collection {
NSNumber *boolNum = [self objectForKey:key inCollection:collection];
return [boolNum boolValue];
}
- (NSData *)dataForKey:(NSString *)key inCollection:(NSString *)collection {
NSData *data = [self objectForKey:key inCollection:collection];
return data;
}
- (ECKeyPair *)keyPairForKey:(NSString *)key inCollection:(NSString *)collection {
ECKeyPair *keyPair = [self objectForKey:key inCollection:collection];
return keyPair;
}
- (PreKeyRecord *)preKeyRecordForKey:(NSString *)key inCollection:(NSString *)collection {
PreKeyRecord *preKeyRecord = [self objectForKey:key inCollection:collection];
return preKeyRecord;
}
- (SignedPreKeyRecord *)signedPreKeyRecordForKey:(NSString *)key inCollection:(NSString *)collection {
SignedPreKeyRecord *preKeyRecord = [self objectForKey:key inCollection:collection];
return preKeyRecord;
}
- (int)intForKey:(NSString *)key inCollection:(NSString *)collection {
int integer = [[self objectForKey:key inCollection:collection] intValue];
return integer;
}
- (void)setInt:(int)integer forKey:(NSString *)key inCollection:(NSString *)collection {
[self setObject:[NSNumber numberWithInt:integer] forKey:key inCollection:collection];
}
- (void)deleteThreadsAndMessages {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[transaction removeAllObjectsInCollection:[TSThread collection]];
[transaction removeAllObjectsInCollection:[SignalRecipient collection]];
[transaction removeAllObjectsInCollection:[TSInteraction collection]];
[transaction removeAllObjectsInCollection:[TSAttachment collection]];
}];
[TSAttachmentStream deleteAttachments];
}
- (void)wipeSignalStorage {
self.database = nil;
NSError *error;
[SAMKeychain deletePasswordForService:keychainService account:keychainDBPassAccount];
2015-12-07 03:31:43 +01:00
[[NSFileManager defaultManager] removeItemAtPath:[self dbPath] error:&error];
if (error) {
DDLogError(@"Failed to delete database: %@", error.description);
}
[TSAttachmentStream deleteAttachments];
[[self init] setupDatabase];
}
#pragma mark - Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
return self.class.tag;
}
2015-12-07 03:31:43 +01:00
@end