From c5079ed3d7c248147cfa94939cc9cd0a542b7bdc Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 24 Jan 2018 16:05:28 -0500 Subject: [PATCH] Add support for key specs. --- Podfile | 1 + Podfile.lock | 2 +- Signal.xcodeproj/project.pbxproj | 6 +- Signal/test/util/OWSDatabaseConverterTest.m | 203 ++++++++++++++++--- SignalMessaging/utils/OWSDatabaseConverter.h | 9 +- SignalMessaging/utils/OWSDatabaseConverter.m | 168 ++++++++------- SignalMessaging/utils/OWSDatabaseUtils.h | 11 + SignalMessaging/utils/OWSDatabaseUtils.mm | 29 +++ SignalServiceKit/src/Storage/OWSStorage.h | 3 + SignalServiceKit/src/Storage/OWSStorage.m | 59 ++++-- 10 files changed, 370 insertions(+), 121 deletions(-) create mode 100644 SignalMessaging/utils/OWSDatabaseUtils.h create mode 100644 SignalMessaging/utils/OWSDatabaseUtils.mm diff --git a/Podfile b/Podfile index 1b02e5d0e..fd76b4635 100644 --- a/Podfile +++ b/Podfile @@ -5,6 +5,7 @@ use_frameworks! def shared_pods # OWS Pods + # pod 'SQLCipher', path: '../sqlcipher2' pod 'SQLCipher', :git => 'https://github.com/sqlcipher/sqlcipher.git', :commit => 'd5c2bec' pod 'YapDatabase/SQLCipher', path: '../YapDatabase' # pod 'YapDatabase/SQLCipher', :git => 'https://github.com/WhisperSystems/YapDatabase.git', branch: 'charlesmchen/signal' diff --git a/Podfile.lock b/Podfile.lock index b32abaf96..2e97d69ba 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -217,6 +217,6 @@ SPEC CHECKSUMS: YapDatabase: 299a32de9d350d37a9ac5b0532609d87d5d2a5de YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54 -PODFILE CHECKSUM: 5d70451d47917767e5120e09e4b5967959971dd6 +PODFILE CHECKSUM: f53f05410c2a8e39055a1513f5d9fd7289f62e0d COCOAPODS: 1.3.1 diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 3863a7d1a..0204a0fbb 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -3146,11 +3146,7 @@ "DEBUG=1", "$(inherited)", ); - "GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = ( - "DEBUG=1", - "$(inherited)", - "SSK_BUILDING_FOR_TESTS=1", - ); + "GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = "DEBUG=1 $(inherited) SSK_BUILDING_FOR_TESTS=1"; GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/Signal/test/util/OWSDatabaseConverterTest.m b/Signal/test/util/OWSDatabaseConverterTest.m index 5fe4f1fc6..75e4e1199 100644 --- a/Signal/test/util/OWSDatabaseConverterTest.m +++ b/Signal/test/util/OWSDatabaseConverterTest.m @@ -52,17 +52,23 @@ NS_ASSUME_NONNULL_BEGIN return [Randomness generateRandomBytes:(int)kSQLCipherSaltLength]; } +- (NSData *)randomDatabaseKeySpec +{ + return [Randomness generateRandomBytes:(int)kSQLCipherKeySpecLength]; +} + // * Open a YapDatabase. // * Do some work with a block. // * Close the database. // * Verify that the database is closed. - (void)openYapDatabase:(NSString *)databaseFilePath - databasePassword:(NSData *)databasePassword + databasePassword:(NSData *_Nullable)databasePassword databaseSalt:(NSData *_Nullable)databaseSalt + databaseKeySpec:(NSData *_Nullable)databaseKeySpec databaseBlock:(void (^_Nonnull)(YapDatabase *))databaseBlock { OWSAssert(databaseFilePath.length > 0); - OWSAssert(databasePassword.length > 0); + OWSAssert(databasePassword.length > 0 || databaseKeySpec.length > 0); OWSAssert(databaseBlock); DDLogVerbose(@"openYapDatabase: %@", databaseFilePath); @@ -75,9 +81,12 @@ NS_ASSUME_NONNULL_BEGIN @autoreleasepool { YapDatabaseOptions *options = [[YapDatabaseOptions alloc] init]; options.corruptAction = YapDatabaseCorruptAction_Fail; - options.cipherKeyBlock = ^{ - return databasePassword; - }; + if (databasePassword) { + DDLogInfo(@"%@ Using password.", self.logTag); + options.cipherKeyBlock = ^{ + return databasePassword; + }; + } options.enableMultiProcessSupport = YES; if (databaseSalt) { @@ -86,6 +95,12 @@ NS_ASSUME_NONNULL_BEGIN return databaseSalt; }; options.cipherUnencryptedHeaderLength = kSqliteHeaderLength; + } else if (databaseKeySpec) { + DDLogInfo(@"%@ Using key spec & unencrypted header.", self.logTag); + options.cipherKeySpecBlock = ^{ + return databaseKeySpec; + }; + options.cipherUnencryptedHeaderLength = kSqliteHeaderLength; } OWSAssert(options.cipherDefaultkdfIterNumber == 0); @@ -147,17 +162,20 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(!strongDatabase); } -- (void)createTestDatabase:(NSString *)databaseFilePath databasePassword:(NSData *)databasePassword +- (void)createTestDatabase:(NSString *)databaseFilePath + databasePassword:(NSData *_Nullable)databasePassword databaseSalt:(NSData *_Nullable)databaseSalt + databaseKeySpec:(NSData *_Nullable)databaseKeySpec { OWSAssert(databaseFilePath.length > 0); - OWSAssert(databasePassword.length > 0); + OWSAssert(databasePassword.length > 0 || databaseKeySpec.length > 0); OWSAssert(![[NSFileManager defaultManager] fileExistsAtPath:databaseFilePath]); [self openYapDatabase:databaseFilePath databasePassword:databasePassword databaseSalt:databaseSalt + databaseKeySpec:databaseKeySpec databaseBlock:^(YapDatabase *database) { [self logHeaderOfDatabaseFile:databaseFilePath label:@"mid-creation"]; @@ -177,11 +195,12 @@ NS_ASSUME_NONNULL_BEGIN } - (BOOL)verifyTestDatabase:(NSString *)databaseFilePath - databasePassword:(NSData *)databasePassword + databasePassword:(NSData *_Nullable)databasePassword databaseSalt:(NSData *_Nullable)databaseSalt + databaseKeySpec:(NSData *_Nullable)databaseKeySpec { OWSAssert(databaseFilePath.length > 0); - OWSAssert(databasePassword.length > 0); + OWSAssert(databasePassword.length > 0 || databaseKeySpec.length > 0); OWSAssert([[NSFileManager defaultManager] fileExistsAtPath:databaseFilePath]); @@ -189,6 +208,7 @@ NS_ASSUME_NONNULL_BEGIN [self openYapDatabase:databaseFilePath databasePassword:databasePassword databaseSalt:databaseSalt + databaseKeySpec:databaseKeySpec databaseBlock:^(YapDatabase *database) { YapDatabaseConnection *dbConnection = database.newConnection; id _Nullable value = [dbConnection objectForKey:@"test_key_name" inCollection:@"test_collection_name"]; @@ -202,8 +222,7 @@ NS_ASSUME_NONNULL_BEGIN - (nullable NSString *)createUnconvertedDatabase:(NSData *)databasePassword { - return [self createDatabase:databasePassword - databaseSalt:nil]; + return [self createDatabase:databasePassword databaseSalt:nil databaseKeySpec:nil]; } - (NSString *)createTempDatabaseFilePath @@ -218,19 +237,28 @@ NS_ASSUME_NONNULL_BEGIN return databaseFilePath; } -// If databaseSalt is nil, creates a non-converted database. +// If databaseSalt and databaseKeySpec are both nil, creates a non-converted database. // Otherwise creates a pre-converted database. -- (nullable NSString *)createDatabase:(NSData *)databasePassword +- (nullable NSString *)createDatabase:(NSData *_Nullable)databasePassword databaseSalt:(NSData *_Nullable)databaseSalt + databaseKeySpec:(NSData *_Nullable)databaseKeySpec { + OWSAssert(databasePassword.length > 0 || databaseKeySpec.length > 0); + NSString *databaseFilePath = [self createTempDatabaseFilePath]; - [self createTestDatabase:databaseFilePath databasePassword:databasePassword databaseSalt:databaseSalt]; + [self createTestDatabase:databaseFilePath + databasePassword:databasePassword + databaseSalt:databaseSalt + databaseKeySpec:databaseKeySpec]; [self logHeaderOfDatabaseFile:databaseFilePath label:@"created"]; - BOOL isValid = [self verifyTestDatabase:databaseFilePath databasePassword:databasePassword databaseSalt:databaseSalt]; + BOOL isValid = [self verifyTestDatabase:databaseFilePath + databasePassword:databasePassword + databaseSalt:databaseSalt + databaseKeySpec:databaseKeySpec]; OWSAssert(isValid); return databaseFilePath; @@ -245,17 +273,70 @@ NS_ASSUME_NONNULL_BEGIN XCTAssertTrue([OWSDatabaseConverter doesDatabaseNeedToBeConverted:databaseFilePath]); } -- (void)testDoesDatabaseNeedToBeConverted_Converted +- (void)testDoesDatabaseNeedToBeConverted_ConvertedWithoutKeyspec { NSData *databasePassword = [self randomDatabasePassword]; NSData *databaseSalt = [self randomDatabaseSalt]; - NSString *_Nullable databaseFilePath = [self createDatabase:databasePassword - databaseSalt:databaseSalt]; + NSData *_Nullable databaseKeySpec = nil; + NSString *_Nullable databaseFilePath = + [self createDatabase:databasePassword databaseSalt:databaseSalt databaseKeySpec:databaseKeySpec]; + XCTAssertFalse([OWSDatabaseConverter doesDatabaseNeedToBeConverted:databaseFilePath]); +} + +- (void)testDoesDatabaseNeedToBeConverted_ConvertedWithKeyspec +{ + NSData *_Nullable databasePassword = nil; + NSData *_Nullable databaseSalt = nil; + NSData *databaseKeySpec = [self randomDatabaseKeySpec]; + NSString *_Nullable databaseFilePath = + [self createDatabase:databasePassword databaseSalt:databaseSalt databaseKeySpec:databaseKeySpec]; + XCTAssertFalse([OWSDatabaseConverter doesDatabaseNeedToBeConverted:databaseFilePath]); +} + +// Verifies that legacy users with non-converted databases can convert. +- (void)testDatabaseConversion_WithoutKeyspec +{ + NSData *databasePassword = [self randomDatabasePassword]; + NSString *_Nullable databaseFilePath = [self createUnconvertedDatabase:databasePassword]; + XCTAssertTrue([OWSDatabaseConverter doesDatabaseNeedToBeConverted:databaseFilePath]); + + __block NSData *_Nullable databaseSalt = nil; + OWSDatabaseSaltBlock saltBlock = ^(NSData *saltData) { + OWSAssert(!databaseSalt); + OWSAssert(saltData); + + databaseSalt = saltData; + }; + __block NSData *_Nullable databaseKeySpec = nil; + OWSDatabaseSaltBlock keySpecBlock = ^(NSData *keySpecData) { + OWSAssert(!databaseKeySpec); + OWSAssert(keySpecData); + + databaseKeySpec = keySpecData; + }; + NSError *_Nullable error = [OWSDatabaseConverter convertDatabaseIfNecessary:databaseFilePath + databasePassword:databasePassword + saltBlock:saltBlock + keySpecBlock:keySpecBlock]; + if (error) { + DDLogError(@"%s error: %@", __PRETTY_FUNCTION__, error); + } + XCTAssertNil(error); XCTAssertFalse([OWSDatabaseConverter doesDatabaseNeedToBeConverted:databaseFilePath]); + XCTAssertNotNil(databaseSalt); + XCTAssertEqual(databaseSalt.length, kSQLCipherSaltLength); + XCTAssertNotNil(databaseKeySpec); + XCTAssertEqual(databaseKeySpec.length, kSQLCipherKeySpecLength); + + BOOL isValid = [self verifyTestDatabase:databaseFilePath + databasePassword:databasePassword + databaseSalt:databaseSalt + databaseKeySpec:nil]; + XCTAssertTrue(isValid); } // Verifies that legacy users with non-converted databases can convert. -- (void)testDatabaseConversion +- (void)testDatabaseConversion_WithKeyspec { NSData *databasePassword = [self randomDatabasePassword]; NSString *_Nullable databaseFilePath = [self createUnconvertedDatabase:databasePassword]; @@ -268,27 +349,80 @@ NS_ASSUME_NONNULL_BEGIN databaseSalt = saltData; }; + __block NSData *_Nullable databaseKeySpec = nil; + OWSDatabaseSaltBlock keySpecBlock = ^(NSData *keySpecData) { + OWSAssert(!databaseKeySpec); + OWSAssert(keySpecData); + + databaseKeySpec = keySpecData; + }; NSError *_Nullable error = [OWSDatabaseConverter convertDatabaseIfNecessary:databaseFilePath databasePassword:databasePassword - saltBlock:saltBlock]; + saltBlock:saltBlock + keySpecBlock:keySpecBlock]; if (error) { DDLogError(@"%s error: %@", __PRETTY_FUNCTION__, error); } XCTAssertNil(error); XCTAssertFalse([OWSDatabaseConverter doesDatabaseNeedToBeConverted:databaseFilePath]); - - BOOL isValid = - [self verifyTestDatabase:databaseFilePath databasePassword:databasePassword databaseSalt:databaseSalt]; + XCTAssertNotNil(databaseSalt); + XCTAssertEqual(databaseSalt.length, kSQLCipherSaltLength); + XCTAssertNotNil(databaseKeySpec); + XCTAssertEqual(databaseKeySpec.length, kSQLCipherKeySpecLength); + + BOOL isValid = [self verifyTestDatabase:databaseFilePath + databasePassword:nil + databaseSalt:nil + databaseKeySpec:databaseKeySpec]; XCTAssertTrue(isValid); } // Verifies new users who create new pre-converted databases. -- (void)testDatabaseCreation +- (void)testDatabaseCreation_WithoutKeySpec { NSData *databasePassword = [self randomDatabasePassword]; NSData *databaseSalt = [self randomDatabaseSalt]; - NSString *_Nullable databaseFilePath = [self createDatabase:databasePassword - databaseSalt:databaseSalt]; + NSData *_Nullable databaseKeySpec = nil; + NSString *_Nullable databaseFilePath = + [self createDatabase:databasePassword databaseSalt:databaseSalt databaseKeySpec:databaseKeySpec]; + XCTAssertFalse([OWSDatabaseConverter doesDatabaseNeedToBeConverted:databaseFilePath]); + + OWSDatabaseSaltBlock saltBlock = ^(NSData *saltData) { + OWSAssert(saltData); + + XCTFail(@"%s No conversion should be necessary", __PRETTY_FUNCTION__); + }; + OWSDatabaseSaltBlock keySpecBlock = ^(NSData *keySpecData) { + OWSAssert(keySpecData); + + XCTFail(@"%s No conversion should be necessary", __PRETTY_FUNCTION__); + }; + + NSError *_Nullable error = [OWSDatabaseConverter convertDatabaseIfNecessary:databaseFilePath + databasePassword:databasePassword + saltBlock:saltBlock + keySpecBlock:keySpecBlock]; + if (error) { + DDLogError(@"%s error: %@", __PRETTY_FUNCTION__, error); + } + XCTAssertNil(error); + XCTAssertFalse([OWSDatabaseConverter doesDatabaseNeedToBeConverted:databaseFilePath]); + + BOOL isValid = [self verifyTestDatabase:databaseFilePath + databasePassword:databasePassword + databaseSalt:databaseSalt + databaseKeySpec:databaseKeySpec]; + XCTAssertTrue(isValid); +} + +// Verifies new users who create new pre-converted databases. +- (void)testDatabaseCreation_WithKeySpec +{ + NSData *_Nullable databasePassword = nil; + NSData *_Nullable databaseSalt = nil; + NSData *databaseKeySpec = [self randomDatabaseKeySpec]; + NSString *_Nullable databaseFilePath = + [self createDatabase:databasePassword databaseSalt:databaseSalt databaseKeySpec:databaseKeySpec]; XCTAssertFalse([OWSDatabaseConverter doesDatabaseNeedToBeConverted:databaseFilePath]); OWSDatabaseSaltBlock saltBlock = ^(NSData *saltData) { @@ -296,17 +430,26 @@ NS_ASSUME_NONNULL_BEGIN XCTFail(@"%s No conversion should be necessary", __PRETTY_FUNCTION__); }; + OWSDatabaseSaltBlock keySpecBlock = ^(NSData *keySpecData) { + OWSAssert(keySpecData); + + XCTFail(@"%s No conversion should be necessary", __PRETTY_FUNCTION__); + }; + NSError *_Nullable error = [OWSDatabaseConverter convertDatabaseIfNecessary:databaseFilePath databasePassword:databasePassword - saltBlock:saltBlock]; + saltBlock:saltBlock + keySpecBlock:keySpecBlock]; if (error) { DDLogError(@"%s error: %@", __PRETTY_FUNCTION__, error); } XCTAssertNil(error); XCTAssertFalse([OWSDatabaseConverter doesDatabaseNeedToBeConverted:databaseFilePath]); - - BOOL isValid = - [self verifyTestDatabase:databaseFilePath databasePassword:databasePassword databaseSalt:databaseSalt]; + + BOOL isValid = [self verifyTestDatabase:databaseFilePath + databasePassword:databasePassword + databaseSalt:databaseSalt + databaseKeySpec:databaseKeySpec]; XCTAssertTrue(isValid); } diff --git a/SignalMessaging/utils/OWSDatabaseConverter.h b/SignalMessaging/utils/OWSDatabaseConverter.h index c62275fbf..3e24ed3a9 100644 --- a/SignalMessaging/utils/OWSDatabaseConverter.h +++ b/SignalMessaging/utils/OWSDatabaseConverter.h @@ -6,8 +6,11 @@ NS_ASSUME_NONNULL_BEGIN extern const NSUInteger kSqliteHeaderLength; extern const NSUInteger kSQLCipherSaltLength; +extern const NSUInteger kSQLCipherDerivedKeyLength; +extern const NSUInteger kSQLCipherKeySpecLength; typedef void (^OWSDatabaseSaltBlock)(NSData *saltData); +typedef void (^OWSDatabaseKeySpecBlock)(NSData *keySpecData); // Used to convert YapDatabase/SQLCipher databases whose header is encrypted // to databases whose first 32 bytes are unencrypted so that iOS can determine @@ -20,7 +23,11 @@ typedef void (^OWSDatabaseSaltBlock)(NSData *saltData); + (nullable NSError *)convertDatabaseIfNecessary; + (nullable NSError *)convertDatabaseIfNecessary:(NSString *)databaseFilePath databasePassword:(NSData *)databasePassword - saltBlock:(OWSDatabaseSaltBlock)saltBlock; + saltBlock:(OWSDatabaseSaltBlock)saltBlock + keySpecBlock:(OWSDatabaseKeySpecBlock)keySpecBlock; + ++ (nullable NSData *)deriveDatabaseKeyForPassword:(NSData *)passwordData saltData:(NSData *)saltData; ++ (nullable NSData *)databaseKeySpecForPassword:(NSData *)passwordData saltData:(NSData *)saltData; @end diff --git a/SignalMessaging/utils/OWSDatabaseConverter.m b/SignalMessaging/utils/OWSDatabaseConverter.m index e9083a359..aed262507 100644 --- a/SignalMessaging/utils/OWSDatabaseConverter.m +++ b/SignalMessaging/utils/OWSDatabaseConverter.m @@ -4,6 +4,7 @@ #import "OWSDatabaseConverter.h" #import "sqlite3.h" +#import #import #import #import @@ -14,6 +15,8 @@ NS_ASSUME_NONNULL_BEGIN const NSUInteger kSqliteHeaderLength = 32; const NSUInteger kSQLCipherSaltLength = 16; +const NSUInteger kSQLCipherDerivedKeyLength = 32; +const NSUInteger kSQLCipherKeySpecLength = 48; @interface OWSStorage (OWSDatabaseConverter) @@ -91,8 +94,14 @@ const NSUInteger kSQLCipherSaltLength = 16; OWSDatabaseSaltBlock saltBlock = ^(NSData *saltData) { [OWSStorage storeDatabaseSalt:saltData]; }; + OWSDatabaseKeySpecBlock keySpecBlock = ^(NSData *keySpecData) { + [OWSStorage storeDatabaseKeySpec:keySpecData]; + }; - return [self convertDatabaseIfNecessary:databaseFilePath databasePassword:databasePassword saltBlock:saltBlock]; + return [self convertDatabaseIfNecessary:databaseFilePath + databasePassword:databasePassword + saltBlock:saltBlock + keySpecBlock:keySpecBlock]; } // TODO upon failure show user error UI @@ -100,38 +109,61 @@ const NSUInteger kSQLCipherSaltLength = 16; + (nullable NSError *)convertDatabaseIfNecessary:(NSString *)databaseFilePath databasePassword:(NSData *)databasePassword saltBlock:(OWSDatabaseSaltBlock)saltBlock + keySpecBlock:(OWSDatabaseKeySpecBlock)keySpecBlock { if (![self doesDatabaseNeedToBeConverted:databaseFilePath]) { return nil; } - return [self convertDatabase:(NSString *)databaseFilePath databasePassword:databasePassword saltBlock:saltBlock]; + return [self convertDatabase:databaseFilePath + databasePassword:databasePassword + saltBlock:saltBlock + keySpecBlock:keySpecBlock]; } + (nullable NSError *)convertDatabase:(NSString *)databaseFilePath databasePassword:(NSData *)databasePassword saltBlock:(OWSDatabaseSaltBlock)saltBlock + keySpecBlock:(OWSDatabaseKeySpecBlock)keySpecBlock { OWSAssert(databaseFilePath.length > 0); OWSAssert(databasePassword.length > 0); OWSAssert(saltBlock); + OWSAssert(keySpecBlock); DDLogVerbose(@"%@ databasePassword: %@", self.logTag, databasePassword.hexadecimalString); - NSData *sqlCipherSaltData; + NSData *saltData; { NSData *headerData = [self readFirstNBytesOfDatabaseFile:databaseFilePath byteCount:kSqliteHeaderLength]; OWSAssert(headerData); OWSAssert(headerData.length >= kSQLCipherSaltLength); - sqlCipherSaltData = [headerData subdataWithRange:NSMakeRange(0, kSQLCipherSaltLength)]; + saltData = [headerData subdataWithRange:NSMakeRange(0, kSQLCipherSaltLength)]; - DDLogVerbose(@"%@ sqlCipherSaltData: %@", self.logTag, sqlCipherSaltData.hexadecimalString); + DDLogVerbose(@"%@ saltData: %@", self.logTag, saltData.hexadecimalString); // Make sure we successfully persist the salt (persumably in the keychain) before // proceeding with the database conversion or we could leave the app in an // unrecoverable state. - saltBlock(sqlCipherSaltData); + saltBlock(saltData); + } + + { + NSData *_Nullable keySpecData = [self databaseKeySpecForPassword:databasePassword saltData:saltData]; + if (!keySpecData || keySpecData.length != kSQLCipherKeySpecLength) { + DDLogError(@"Error deriving key spec"); + return OWSErrorWithCodeDescription(OWSErrorCodeDatabaseConversionFatalError, @"Invalid key spec"); + } + + DDLogVerbose(@"%@ keySpecData: %@", self.logTag, keySpecData.hexadecimalString); + + OWSAssert(keySpecData.length == kSQLCipherKeySpecLength); + + // Make sure we successfully persist the key spec (persumably in the keychain) before + // proceeding with the database conversion or we could leave the app in an + // unrecoverable state. + keySpecBlock(keySpecData); } // ----------------------------------------------------------- @@ -237,72 +269,7 @@ const NSUInteger kSQLCipherSaltLength = 16; DDLogVerbose(@"%@ saltString: %@", self.logTag, saltString); - OWSAssert([sqlCipherSaltData.hexadecimalString isEqualToString:saltString]); - } - // We can obtain the database salt in two ways: by reading the first 16 bytes of the encrypted - // header OR by using "PRAGMA cipher_salt". In DEBUG builds, we verify that these two values - // match. - { - - // - // In practice, for a real application, the other changes we talked about on the phone need occur, i.e. - // to provide the salt to the application explicitly. The application can use a raw key spec, where the - // 96 hex are provide (i.e. 64 hex for the 256 bit key, followed by 32 hex for the 128 bit salt) using - // explicit BLOB syntax, e.g. - // - // x'98483C6EB40B6C31A448C22A66DED3B5E5E8D5119CAC8327B655C8B5C483648101010101010101010101010101010101' - extern void sqlite3CodecGetKey(sqlite3 * db, int nDb, void **zKey, int *nKey); - char *keySpecBytes = NULL; - int keySpecLength = 0; - sqlite3CodecGetKey(db, 0, (void **)&keySpecBytes, &keySpecLength); - if (!keySpecBytes || keySpecLength < 1) { - DDLogError(@"Error extracting key spec."); - return OWSErrorWithCodeDescription(OWSErrorCodeDatabaseConversionFatalError, @"Failed to extract key spec"); - } - NSData *_Nullable keySpecData = [NSData dataWithBytes:keySpecBytes length:keySpecLength]; - if (!keySpecData) { - DDLogError(@"Invalid key spec."); - return OWSErrorWithCodeDescription(OWSErrorCodeDatabaseConversionFatalError, @"Invalid key spec"); - } - DDLogInfo(@"keySpecData: %@", keySpecData.hexadecimalString); - - - // SQLITE_PRIVATE void sqlite3CodecGetKey(sqlite3* db, int nDb, void **zKey, int *nKey) { - // struct Db *pDb = &db->aDb[nDb]; - // CODEC_TRACE("sqlite3CodecGetKey: entered db=%p, nDb=%d\n", db, nDb); - // if( pDb->pBt ) { - // codec_ctx *ctx; - // sqlite3pager_get_codec(pDb->pBt->pBt->pPager, (void **) &ctx); - // if(ctx) { - // if(sqlcipher_codec_get_store_pass(ctx) == 1) { - // sqlcipher_codec_get_pass(ctx, zKey, nKey); - // } else { - // sqlcipher_codec_get_keyspec(ctx, zKey, nKey); - // } - // } else { - // *zKey = NULL; - // *nKey = 0; - // } - // } - // } - // - // codec_ctx *ctx; - // - // char *keySpecBytes = NULL; - // int keySpecLength = 0; - //// extern void sqlite3CodecGetKey(sqlite3*, int, void**, int*); - // sqlcipher_codec_get_keyspec(ctx, (void**)&keySpecBytes, &keySpecLength); - - // NSString *saltString = - // [[NSString alloc] initWithBytes:valueBytes length:(NSUInteger) valueLength - // encoding:NSUTF8StringEncoding]; - // - // sqlite3_finalize(statement); - // statement = NULL; - // - // DDLogVerbose(@"%@ saltString: %@", self.logTag, saltString); - // - // OWSAssert([sqlCipherSaltData.hexadecimalString isEqualToString:saltString]); + OWSAssert([saltData.hexadecimalString isEqualToString:saltString]); } #endif @@ -398,6 +365,63 @@ const NSUInteger kSQLCipherSaltLength = 16; return result; } ++ (nullable NSData *)deriveDatabaseKeyForPassword:(NSData *)passwordData saltData:(NSData *)saltData +{ + OWSAssert(passwordData.length > 0); + OWSAssert(saltData.length == kSQLCipherSaltLength); + + unsigned char *derivedKeyBytes = malloc((size_t)kSQLCipherDerivedKeyLength); + OWSAssert(derivedKeyBytes); + // See: PBKDF2_ITER. + const unsigned int workfactor = 64000; + + int result = CCKeyDerivationPBKDF(kCCPBKDF2, + passwordData.bytes, + (size_t)passwordData.length, + saltData.bytes, + (size_t)saltData.length, + kCCPRFHmacAlgSHA1, + workfactor, + derivedKeyBytes, + kSQLCipherDerivedKeyLength); + if (result != kCCSuccess) { + DDLogError(@"Error deriving key: %d", result); + return nil; + } + + NSData *_Nullable derivedKeyData = [NSData dataWithBytes:derivedKeyBytes length:kSQLCipherDerivedKeyLength]; + if (!derivedKeyData || derivedKeyData.length != kSQLCipherDerivedKeyLength) { + DDLogError(@"Invalid derived key: %d", result); + return nil; + } + DDLogVerbose(@"%@ derivedKeyData: %@", self.logTag, derivedKeyData.hexadecimalString); + + return derivedKeyData; +} + ++ (nullable NSData *)databaseKeySpecForPassword:(NSData *)passwordData saltData:(NSData *)saltData +{ + OWSAssert(passwordData.length > 0); + OWSAssert(saltData.length == kSQLCipherSaltLength); + + NSData *_Nullable derivedKeyData = [self deriveDatabaseKeyForPassword:passwordData saltData:saltData]; + if (!derivedKeyData || derivedKeyData.length != kSQLCipherDerivedKeyLength) { + DDLogError(@"Error deriving key"); + return nil; + } + DDLogVerbose(@"%@ derivedKeyData: %@", self.logTag, derivedKeyData.hexadecimalString); + + NSMutableData *keySpecData = [NSMutableData new]; + [keySpecData appendData:derivedKeyData]; + [keySpecData appendData:saltData]; + + DDLogVerbose(@"%@ keySpecData: %@", self.logTag, keySpecData.hexadecimalString); + + OWSAssert(keySpecData.length == kSQLCipherKeySpecLength); + + return keySpecData; +} + @end NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/utils/OWSDatabaseUtils.h b/SignalMessaging/utils/OWSDatabaseUtils.h new file mode 100644 index 000000000..b5dfc0f64 --- /dev/null +++ b/SignalMessaging/utils/OWSDatabaseUtils.h @@ -0,0 +1,11 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "sqlite3.h" + +NS_ASSUME_NONNULL_BEGIN + +NSData *_Nullable ExtractDatabaseKeySpec(sqlite3 *db); + +NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/utils/OWSDatabaseUtils.mm b/SignalMessaging/utils/OWSDatabaseUtils.mm new file mode 100644 index 000000000..5bf640c25 --- /dev/null +++ b/SignalMessaging/utils/OWSDatabaseUtils.mm @@ -0,0 +1,29 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSDatabaseUtils.h" + +NS_ASSUME_NONNULL_BEGIN + +extern "C" { +extern void sqlite3CodecGetKey(sqlite3 *db, int nDb, void **zKey, int *nKey); +} + +NSData *_Nullable ExtractDatabaseKeySpec(sqlite3 *db) +{ + char *keySpecBytes = NULL; + int keySpecLength = 0; + sqlite3CodecGetKey(db, 0, (void **)&keySpecBytes, &keySpecLength); + if (!keySpecBytes || keySpecLength < 1) { + return nil; + } + NSData *_Nullable keySpecData = [NSData dataWithBytes:keySpecBytes length:(NSUInteger)keySpecLength]; + if (!keySpecData) { + return nil; + } + + return keySpecData; +} + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Storage/OWSStorage.h b/SignalServiceKit/src/Storage/OWSStorage.h index a8458b736..a39a42ebe 100644 --- a/SignalServiceKit/src/Storage/OWSStorage.h +++ b/SignalServiceKit/src/Storage/OWSStorage.h @@ -60,6 +60,9 @@ extern NSString *const StorageIsReadyNotification; + (nullable NSData *)tryToLoadDatabaseSalt:(NSError **)errorHandle; + (void)storeDatabaseSalt:(NSData *)saltData; ++ (nullable NSData *)tryToLoadDatabaseKeySpec:(NSError **)errorHandle; ++ (void)storeDatabaseKeySpec:(NSData *)keySpecData; + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Storage/OWSStorage.m b/SignalServiceKit/src/Storage/OWSStorage.m index b6caa4f3a..49b1a47a4 100644 --- a/SignalServiceKit/src/Storage/OWSStorage.m +++ b/SignalServiceKit/src/Storage/OWSStorage.m @@ -28,10 +28,13 @@ NSString *const OWSResetStorageNotification = @"OWSResetStorageNotification"; static NSString *keychainService = @"TSKeyChainService"; static NSString *keychainDBPassAccount = @"TSDatabasePass"; static NSString *keychainDBSalt = @"OWSDatabaseSalt"; +static NSString *keychainDBKeySpec = @"OWSDatabaseKeySpec"; -// TODO: Move this to YapDatabase. +// TODO: Move these constants to YapDatabase. const NSUInteger kSqliteHeaderLength = 32; const NSUInteger kSQLCipherSaltLength = 16; +const NSUInteger kSQLCipherKeySpecLength = 48; + const NSUInteger kDatabasePasswordLength = 30; typedef NSData *_Nullable (^LoadDatabaseMetadataBlock)(NSError **_Nullable); @@ -382,23 +385,17 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void); - (BOOL)tryToLoadDatabase { - - // We determine the database password first, since a side effect of + // We determine the database password / key spec 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]; - OWSAssert(databasePassword.length > 0); - NSData *databaseSalt = [self databaseSalt]; - OWSAssert(databaseSalt.length > 0); + NSData *databaseKeySpec = [self databaseKeySpec]; + OWSAssert(databaseKeySpec.length == kSQLCipherKeySpecLength); YapDatabaseOptions *options = [[YapDatabaseOptions alloc] init]; options.corruptAction = YapDatabaseCorruptAction_Fail; - options.cipherKeyBlock = ^{ - return databasePassword; - }; options.enableMultiProcessSupport = YES; - options.cipherSaltBlock = ^{ - return databaseSalt; + options.cipherKeySpecBlock = ^{ + return databaseKeySpec; }; options.cipherUnencryptedHeaderLength = kSqliteHeaderLength; @@ -555,6 +552,11 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void); return [self tryToLoadKeyChainValue:keychainDBSalt errorHandle:errorHandle]; } ++ (nullable NSData *)tryToLoadDatabaseKeySpec:(NSError **)errorHandle +{ + return [self tryToLoadKeyChainValue:keychainDBKeySpec errorHandle:errorHandle]; +} + - (NSData *)databasePassword { return [self loadMetadataOrClearDatabase:^(NSError **_Nullable errorHandle) { @@ -577,6 +579,17 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void); label:@"Database salt"]; } +- (NSData *)databaseKeySpec +{ + return [self loadMetadataOrClearDatabase:^(NSError **_Nullable errorHandle) { + return [OWSStorage tryToLoadDatabaseKeySpec:errorHandle]; + } + createDataBlock:^{ + return [self createAndSetNewDatabaseKeySpec]; + } + label:@"Database key spec"]; +} + - (NSData *)loadMetadataOrClearDatabase:(LoadDatabaseMetadataBlock)loadDataBlock createDataBlock:(CreateDatabaseMetadataBlock)createDataBlock label:(NSString *)label @@ -653,6 +666,21 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void); return saltData; } +- (NSData *)createAndSetNewDatabaseKeySpec +{ + NSData *databasePassword = [self databasePassword]; + OWSAssert(databasePassword.length > 0); + NSData *databaseSalt = [self databaseSalt]; + OWSAssert(databaseSalt.length == kSQLCipherSaltLength); + + NSData *keySpecData = [OWSDatabaseConverter databaseKeySpecForPassword:databasePassword saltData:databaseSalt]; + OWSAssert(keySpecData.length == kSQLCipherKeySpecLength); + + [OWSStorage storeDatabaseKeySpec:keySpecData]; + + return keySpecData; +} + - (void)backgroundedAppDatabasePasswordInaccessibleWithErrorDescription:(NSString *)errorDescription { OWSAssert(CurrentAppContext().isMainApp && CurrentAppContext().isInBackground); @@ -722,6 +750,13 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void); [self storeKeyChainValue:saltData keychainKey:keychainDBSalt]; } ++ (void)storeDatabaseKeySpec:(NSData *)keySpecData +{ + OWSAssert(keySpecData.length == kSQLCipherKeySpecLength); + + [self storeKeyChainValue:keySpecData keychainKey:keychainDBKeySpec]; +} + @end NS_ASSUME_NONNULL_END