|
|
@ -4,42 +4,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
#import "OWSDatabaseConverter.h"
|
|
|
|
#import "OWSDatabaseConverter.h"
|
|
|
|
#import "sqlite3.h"
|
|
|
|
#import "sqlite3.h"
|
|
|
|
|
|
|
|
#import <SignalServiceKit/NSData+hexString.h>
|
|
|
|
#import <SignalServiceKit/OWSError.h>
|
|
|
|
#import <SignalServiceKit/OWSError.h>
|
|
|
|
#import <SignalServiceKit/OWSFileSystem.h>
|
|
|
|
#import <SignalServiceKit/OWSFileSystem.h>
|
|
|
|
#import <SignalServiceKit/TSStorageManager.h>
|
|
|
|
#import <SignalServiceKit/TSStorageManager.h>
|
|
|
|
|
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
|
|
|
|
|
|
|
const int kSqliteHeaderLength = 32;
|
|
|
|
const NSUInteger kSqliteHeaderLength = 32;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@interface OWSStorage (OWSDatabaseConverter)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+ (YapDatabaseDeserializer)logOnFailureDeserializer;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
|
|
|
|
@implementation OWSDatabaseConverter
|
|
|
|
@implementation OWSDatabaseConverter
|
|
|
|
|
|
|
|
|
|
|
|
+ (BOOL)doesDatabaseNeedToBeConverted:(NSString *)databaseFilePath
|
|
|
|
+ (NSData *)readFirstNBytesOfDatabaseFile:(NSString *)filePath byteCount:(NSUInteger)byteCount
|
|
|
|
{
|
|
|
|
{
|
|
|
|
OWSAssert(databaseFilePath.length > 0);
|
|
|
|
OWSAssert(filePath.length > 0);
|
|
|
|
|
|
|
|
|
|
|
|
if (![[NSFileManager defaultManager] fileExistsAtPath:databaseFilePath]) {
|
|
|
|
@autoreleasepool {
|
|
|
|
DDLogVerbose(@"%@ Skipping database conversion; no legacy database found.", self.logTag);
|
|
|
|
|
|
|
|
return NO;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
NSError *error;
|
|
|
|
NSError *error;
|
|
|
|
// We use NSDataReadingMappedAlways instead of NSDataReadingMappedIfSafe because
|
|
|
|
// We use NSDataReadingMappedAlways instead of NSDataReadingMappedIfSafe because
|
|
|
|
// we know the database will always exist for the duration of this instance of NSData.
|
|
|
|
// we know the database will always exist for the duration of this instance of NSData.
|
|
|
|
NSData *_Nullable data = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:databaseFilePath]
|
|
|
|
NSData *_Nullable data = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:filePath]
|
|
|
|
options:NSDataReadingMappedAlways
|
|
|
|
options:NSDataReadingMappedAlways
|
|
|
|
error:&error];
|
|
|
|
error:&error];
|
|
|
|
if (!data || error) {
|
|
|
|
if (!data || error) {
|
|
|
|
DDLogError(@"%@ Couldn't read legacy database file header.", self.logTag);
|
|
|
|
DDLogError(@"%@ Couldn't read database file header.", self.logTag);
|
|
|
|
// TODO: Make a convenience method (on a category of NSException?) that
|
|
|
|
// TODO: Make a convenience method (on a category of NSException?) that
|
|
|
|
// flushes DDLog before raising a terminal exception.
|
|
|
|
// flushes DDLog before raising a terminal exception.
|
|
|
|
[NSException raise:@"Couldn't read legacy database file header" format:@""];
|
|
|
|
[NSException raise:@"Couldn't read database file header" format:@""];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Pull this constant out so that we can use it in our YapDatabase fork.
|
|
|
|
// Pull this constant out so that we can use it in our YapDatabase fork.
|
|
|
|
NSData *_Nullable headerData = [data subdataWithRange:NSMakeRange(0, kSqliteHeaderLength)];
|
|
|
|
NSData *_Nullable headerData = [data subdataWithRange:NSMakeRange(0, byteCount)];
|
|
|
|
if (!headerData || headerData.length != kSqliteHeaderLength) {
|
|
|
|
if (!headerData || headerData.length != byteCount) {
|
|
|
|
[NSException raise:@"Database database file header has unexpected length"
|
|
|
|
[NSException raise:@"Database file header has unexpected length"
|
|
|
|
format:@"Database database file header has unexpected length: %zd", headerData.length];
|
|
|
|
format:@"Database file header has unexpected length: %zd", headerData.length];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return [headerData copy];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+ (BOOL)doesDatabaseNeedToBeConverted:(NSString *)databaseFilePath
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
OWSAssert(databaseFilePath.length > 0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (![[NSFileManager defaultManager] fileExistsAtPath:databaseFilePath]) {
|
|
|
|
|
|
|
|
DDLogVerbose(@"%@ database file not found.", self.logTag);
|
|
|
|
|
|
|
|
return nil;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NSData *headerData = [self readFirstNBytesOfDatabaseFile:databaseFilePath byteCount:kSqliteHeaderLength];
|
|
|
|
|
|
|
|
OWSAssert(headerData);
|
|
|
|
|
|
|
|
|
|
|
|
NSString *kUnencryptedHeader = @"SQLite format 3\0";
|
|
|
|
NSString *kUnencryptedHeader = @"SQLite format 3\0";
|
|
|
|
NSData *unencryptedHeaderData = [kUnencryptedHeader dataUsingEncoding:NSUTF8StringEncoding];
|
|
|
|
NSData *unencryptedHeaderData = [kUnencryptedHeader dataUsingEncoding:NSUTF8StringEncoding];
|
|
|
|
BOOL isUnencrypted = [unencryptedHeaderData
|
|
|
|
BOOL isUnencrypted = [unencryptedHeaderData
|
|
|
@ -52,36 +74,46 @@ const int kSqliteHeaderLength = 32;
|
|
|
|
return YES;
|
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
+ (void)convertDatabaseIfNecessary
|
|
|
|
+ (nullable NSError *)convertDatabaseIfNecessary
|
|
|
|
{
|
|
|
|
{
|
|
|
|
NSString *databaseFilePath = [TSStorageManager legacyDatabaseFilePath];
|
|
|
|
NSString *databaseFilePath = [TSStorageManager legacyDatabaseFilePath];
|
|
|
|
[self convertDatabaseIfNecessary:databaseFilePath];
|
|
|
|
|
|
|
|
|
|
|
|
NSError *error;
|
|
|
|
|
|
|
|
NSData *_Nullable databasePassword = [OWSStorage tryToLoadDatabasePassword:&error];
|
|
|
|
|
|
|
|
if (!databasePassword || error) {
|
|
|
|
|
|
|
|
return (error
|
|
|
|
|
|
|
|
?: OWSErrorWithCodeDescription(
|
|
|
|
|
|
|
|
OWSErrorCodeDatabaseConversionFatalError, @"Failed to load database password"));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return [self convertDatabaseIfNecessary:databaseFilePath databasePassword:databasePassword];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TODO upon failure show user error UI
|
|
|
|
// TODO upon failure show user error UI
|
|
|
|
// TODO upon failure anything we need to do "back out" partial migration
|
|
|
|
// TODO upon failure anything we need to do "back out" partial migration
|
|
|
|
+ (void)convertDatabaseIfNecessary:(NSString *)databaseFilePath
|
|
|
|
+ (nullable NSError *)convertDatabaseIfNecessary:(NSString *)databaseFilePath
|
|
|
|
|
|
|
|
databasePassword:(NSData *)databasePassword
|
|
|
|
{
|
|
|
|
{
|
|
|
|
if (![self doesDatabaseNeedToBeConverted:databaseFilePath]) {
|
|
|
|
if (![self doesDatabaseNeedToBeConverted:databaseFilePath]) {
|
|
|
|
return;
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[self convertDatabase:(NSString *)databaseFilePath];
|
|
|
|
return [self convertDatabase:(NSString *)databaseFilePath databasePassword:databasePassword];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
+ (nullable NSError *)convertDatabase:(NSString *)databaseFilePath
|
|
|
|
+ (nullable NSError *)convertDatabase:(NSString *)databaseFilePath databasePassword:(NSData *)databasePassword
|
|
|
|
{
|
|
|
|
{
|
|
|
|
OWSAssert(databaseFilePath.length > 0);
|
|
|
|
OWSAssert(databaseFilePath.length > 0);
|
|
|
|
|
|
|
|
OWSAssert(databasePassword.length > 0);
|
|
|
|
|
|
|
|
|
|
|
|
NSError *error;
|
|
|
|
NSData *headerData = [self readFirstNBytesOfDatabaseFile:databaseFilePath byteCount:kSqliteHeaderLength];
|
|
|
|
NSData *_Nullable databasePassword = [OWSStorage tryToLoadDatabasePassword:&error];
|
|
|
|
OWSAssert(headerData);
|
|
|
|
if (!databasePassword || error) {
|
|
|
|
|
|
|
|
return (error
|
|
|
|
|
|
|
|
?: OWSErrorWithCodeDescription(
|
|
|
|
|
|
|
|
OWSErrorCodeDatabaseConversionFatalError, @"Failed to load database password"));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TODO:
|
|
|
|
const NSUInteger kSQLCipherSaltLength = 16;
|
|
|
|
|
|
|
|
OWSAssert(headerData.length >= kSQLCipherSaltLength);
|
|
|
|
|
|
|
|
NSData *sqlCipherSaltData = [headerData subdataWithRange:NSMakeRange(0, kSQLCipherSaltLength)];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: Write salt to keychain.
|
|
|
|
|
|
|
|
|
|
|
|
// Hello Matthew,
|
|
|
|
// Hello Matthew,
|
|
|
|
//
|
|
|
|
//
|
|
|
@ -181,13 +213,10 @@ const int kSqliteHeaderLength = 32;
|
|
|
|
return OWSErrorWithCodeDescription(OWSErrorCodeDatabaseConversionFatalError, @"Failed to set SQLCipher key");
|
|
|
|
return OWSErrorWithCodeDescription(OWSErrorCodeDatabaseConversionFatalError, @"Failed to set SQLCipher key");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TODO set plaintext pragma
|
|
|
|
|
|
|
|
// TODO modify first page
|
|
|
|
|
|
|
|
// TODO force checkpoint
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------
|
|
|
|
// -----------------------------------------------------------
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// This block was derived from [Yapdatabase configureDatabase].
|
|
|
|
// This block was derived from [Yapdatabase configureDatabase].
|
|
|
|
|
|
|
|
{
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// {
|
|
|
|
// {
|
|
|
|
// int status;
|
|
|
|
// int status;
|
|
|
@ -287,33 +316,53 @@ const int kSqliteHeaderLength = 32;
|
|
|
|
|
|
|
|
|
|
|
|
// END DB setup copied from YapDatabase
|
|
|
|
// END DB setup copied from YapDatabase
|
|
|
|
// BEGIN SQLCipher migration
|
|
|
|
// BEGIN SQLCipher migration
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
NSString *setPlainTextHeaderPragma =
|
|
|
|
|
|
|
|
[NSString stringWithFormat:@"PRAGMA cipher_plaintext_header_size = %d;", kSqliteHeaderLength];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
status = sqlite3_exec(db, [setPlainTextHeaderPragma UTF8String], NULL, NULL, NULL);
|
|
|
|
// -----------------------------------------------------------
|
|
|
|
if (status != SQLITE_OK) {
|
|
|
|
//
|
|
|
|
DDLogError(@"Error setting PRAGMA cipher_plaintext_header_size = %d: status: %d, error: %s",
|
|
|
|
// SQLCipher migration
|
|
|
|
kSqliteHeaderLength,
|
|
|
|
|
|
|
|
status,
|
|
|
|
|
|
|
|
sqlite3_errmsg(db));
|
|
|
|
|
|
|
|
return OWSErrorWithCodeDescription(
|
|
|
|
|
|
|
|
OWSErrorCodeDatabaseConversionFatalError, @"Failed to set PRAGMA cipher_plaintext_header_size");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Modify the first page, so that SQLCipher will overwrite, respecting our new cipher_plaintext_header_size
|
|
|
|
// if (NO)
|
|
|
|
NSString *tableName = [NSString stringWithFormat:@"signal-migration-%@", [NSUUID new].UUIDString];
|
|
|
|
// {
|
|
|
|
NSString *modificationSQL =
|
|
|
|
//
|
|
|
|
[NSString stringWithFormat:@"CREATE TABLE %@(int a); INSERT INTO %@(a) VALUES (1);", tableName, tableName];
|
|
|
|
// NSString *setPlainTextHeaderPragma =
|
|
|
|
status = sqlite3_exec(db, [modificationSQL UTF8String], NULL, NULL, NULL);
|
|
|
|
// [NSString stringWithFormat:@"PRAGMA cipher_plaintext_header_size = %zd;", kSqliteHeaderLength];
|
|
|
|
if (status != SQLITE_OK) {
|
|
|
|
//
|
|
|
|
DDLogError(@"%@ Error modifying first page: %d, error: %s", self.logTag, status, sqlite3_errmsg(db));
|
|
|
|
// status = sqlite3_exec(db, [setPlainTextHeaderPragma UTF8String], NULL, NULL, NULL);
|
|
|
|
return OWSErrorWithCodeDescription(OWSErrorCodeDatabaseConversionFatalError, @"Error modifying first page");
|
|
|
|
// if (status != SQLITE_OK) {
|
|
|
|
}
|
|
|
|
// DDLogError(@"Error setting PRAGMA cipher_plaintext_header_size = %zd: status: %d, error: %s",
|
|
|
|
|
|
|
|
// kSqliteHeaderLength,
|
|
|
|
|
|
|
|
// status,
|
|
|
|
|
|
|
|
// sqlite3_errmsg(db));
|
|
|
|
|
|
|
|
// return OWSErrorWithCodeDescription(
|
|
|
|
|
|
|
|
// OWSErrorCodeDatabaseConversionFatalError, @"Failed to set PRAGMA
|
|
|
|
|
|
|
|
// cipher_plaintext_header_size");
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// // Modify the first page, so that SQLCipher will overwrite, respecting our new cipher_plaintext_header_size
|
|
|
|
|
|
|
|
// NSString *tableName = [NSString stringWithFormat:@"signal-migration-%@", [NSUUID new].UUIDString];
|
|
|
|
|
|
|
|
// NSString *modificationSQL =
|
|
|
|
|
|
|
|
// [NSString stringWithFormat:@"CREATE TABLE %@(int a); INSERT INTO %@(a) VALUES (1);", tableName, tableName];
|
|
|
|
|
|
|
|
// status = sqlite3_exec(db, [modificationSQL UTF8String], NULL, NULL, NULL);
|
|
|
|
|
|
|
|
// if (status != SQLITE_OK) {
|
|
|
|
|
|
|
|
// DDLogError(@"%@ Error modifying first page: %d, error: %s", self.logTag, status, sqlite3_errmsg(db));
|
|
|
|
|
|
|
|
// return OWSErrorWithCodeDescription(OWSErrorCodeDatabaseConversionFatalError, @"Error modifying first
|
|
|
|
|
|
|
|
// page");
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// // Force a checkpoint so that the plaintext is written to the actual DB file, not just living in the WAL.
|
|
|
|
|
|
|
|
// // TODO do we need/want the earlier checkpoint if we're checkpointing here?
|
|
|
|
|
|
|
|
// sqlite3_wal_autocheckpoint(db, 0);
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// sqlite3_close(db);
|
|
|
|
|
|
|
|
// return nil;
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
// Force a checkpoint so that the plaintext is written to the actual DB file, not just living in the WAL.
|
|
|
|
// TODO set plaintext pragma
|
|
|
|
// TODO do we need/want the earlier checkpoint if we're checkpointing here?
|
|
|
|
// TODO modify first page
|
|
|
|
sqlite3_wal_autocheckpoint(db, 0);
|
|
|
|
// TODO force checkpoint
|
|
|
|
|
|
|
|
|
|
|
|
return nil;
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
}
|
|
|
|