Add support for key specs.
This commit is contained in:
parent
224c24e685
commit
c5079ed3d7
1
Podfile
1
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'
|
||||
|
|
|
@ -217,6 +217,6 @@ SPEC CHECKSUMS:
|
|||
YapDatabase: 299a32de9d350d37a9ac5b0532609d87d5d2a5de
|
||||
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
|
||||
|
||||
PODFILE CHECKSUM: 5d70451d47917767e5120e09e4b5967959971dd6
|
||||
PODFILE CHECKSUM: f53f05410c2a8e39055a1513f5d9fd7289f62e0d
|
||||
|
||||
COCOAPODS: 1.3.1
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
- (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_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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
#import "OWSDatabaseConverter.h"
|
||||
#import "sqlite3.h"
|
||||
#import <CommonCrypto/CommonCrypto.h>
|
||||
#import <SignalServiceKit/NSData+hexString.h>
|
||||
#import <SignalServiceKit/OWSError.h>
|
||||
#import <SignalServiceKit/OWSFileSystem.h>
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue