Merge branch 'mkirk/keygen-revamp'

pull/1/head
Michael Kirk 7 years ago
commit d4c20ad5c5

@ -193,7 +193,7 @@ CHECKOUT OPTIONS:
:commit: d5c2bec :commit: d5c2bec
:git: https://github.com/sqlcipher/sqlcipher.git :git: https://github.com/sqlcipher/sqlcipher.git
YapDatabase: YapDatabase:
:commit: eaff655ebc774105e83f835ead71f8b7a02e4ac1 :commit: bdd7409de45f9e38b9144adba3b38d74ca48ea77
:git: https://github.com/WhisperSystems/YapDatabase.git :git: https://github.com/WhisperSystems/YapDatabase.git
SPEC CHECKSUMS: SPEC CHECKSUMS:

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>classNames</key>
<dict>
<key>OWSDatabaseConverterTest</key>
<dict>
<key>testGranularKeySpecFetchingStrategy</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.039171</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
<key>testGranularPassphraseFetchingStrategy</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.22846</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
<key>testWideKeyFetchingStrategy</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.039649</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
<key>testWidePassphraseFetchingStrategy</key>
<dict>
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
<dict>
<key>baselineAverage</key>
<real>0.21819</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
</dict>
</dict>
</dict>
</plist>

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>runDestinationsByUUID</key>
<dict>
<key>8A553EB1-B9DF-4DDE-8F93-10474ECF05C2</key>
<dict>
<key>localComputer</key>
<dict>
<key>busSpeedInMHz</key>
<integer>100</integer>
<key>cpuCount</key>
<integer>1</integer>
<key>cpuKind</key>
<string>Intel Core i7</string>
<key>cpuSpeedInMHz</key>
<integer>2900</integer>
<key>logicalCPUCoresPerPackage</key>
<integer>8</integer>
<key>modelCode</key>
<string>MacBookPro13,3</string>
<key>physicalCPUCoresPerPackage</key>
<integer>4</integer>
<key>platformIdentifier</key>
<string>com.apple.platform.macosx</string>
</dict>
<key>targetArchitecture</key>
<string>x86_64</string>
<key>targetDevice</key>
<dict>
<key>modelCode</key>
<string>iPhone8,1</string>
<key>platformIdentifier</key>
<string>com.apple.platform.iphonesimulator</string>
</dict>
</dict>
</dict>
</dict>
</plist>

@ -251,28 +251,40 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
- (nullable NSError *)convertDatabaseIfNecessary - (nullable NSError *)convertDatabaseIfNecessary
{ {
NSString *databaseFilePath = [TSStorageManager legacyDatabaseFilePath]; NSString *databaseFilePath = [TSStorageManager legacyDatabaseFilePath];
if (![[NSFileManager defaultManager] fileExistsAtPath:databaseFilePath]) {
DDLogVerbose(@"%@ no legacy database file found", self.logTag);
return nil;
}
NSError *error; NSError *error;
NSData *_Nullable databasePassword = [OWSStorage tryToLoadDatabasePassword:&error]; NSData *_Nullable databasePassword = [OWSStorage tryToLoadDatabaseLegacyPassphrase:&error];
if (!databasePassword || error) { if (!databasePassword || error) {
return (error return (error
?: OWSErrorWithCodeDescription( ?: OWSErrorWithCodeDescription(
OWSErrorCodeDatabaseConversionFatalError, @"Failed to load database password")); OWSErrorCodeDatabaseConversionFatalError, @"Failed to load database password"));
} }
YapDatabaseSaltBlock saltBlock = ^(NSData *saltData) { YapRecordDatabaseSaltBlock recordSaltBlock = ^(NSData *saltData) {
DDLogVerbose(@"%@ saltData: %@", self.logTag, saltData.hexadecimalString); DDLogVerbose(@"%@ saltData: %@", self.logTag, saltData.hexadecimalString);
[OWSStorage storeDatabaseSalt:saltData];
}; // Derive and store the raw cipher key spec, to avoid the ongoing tax of future KDF
YapDatabaseKeySpecBlock keySpecBlock = ^(NSData *keySpecData) { NSData *_Nullable keySpecData =
DDLogVerbose(@"%@ keySpecData: %@", self.logTag, keySpecData.hexadecimalString); [YapDatabaseCryptoUtils deriveDatabaseKeySpecForPassword:databasePassword saltData:saltData];
[OWSStorage storeDatabaseKeySpec:keySpecData];
if (!keySpecData) {
DDLogError(@"%@ Failed to derive key spec.", self.logTag);
return NO;
}
[OWSStorage storeDatabaseCipherKeySpec:keySpecData];
[OWSStorage removeLegacyPassphrase];
return YES;
}; };
return [YapDatabaseCryptoUtils convertDatabaseIfNecessary:databaseFilePath return [YapDatabaseCryptoUtils convertDatabaseIfNecessary:databaseFilePath
databasePassword:databasePassword databasePassword:databasePassword
saltBlock:saltBlock recordSaltBlock:recordSaltBlock];
keySpecBlock:keySpecBlock];
} }
- (void)startupLogging - (void)startupLogging

@ -17,6 +17,8 @@ NS_ASSUME_NONNULL_BEGIN
@interface OWSStorage (OWSDatabaseConverterTest) @interface OWSStorage (OWSDatabaseConverterTest)
+ (YapDatabaseDeserializer)logOnFailureDeserializer; + (YapDatabaseDeserializer)logOnFailureDeserializer;
+ (void)storeKeyChainValue:(NSData *)data keychainKey:(NSString *)keychainKey;
+ (nullable NSData *)tryToLoadKeyChainValue:(NSString *)keychainKey errorHandle:(NSError **)errorHandle;
@end @end
@ -298,23 +300,21 @@ NS_ASSUME_NONNULL_BEGIN
XCTAssertTrue([YapDatabaseCryptoUtils doesDatabaseNeedToBeConverted:databaseFilePath]); XCTAssertTrue([YapDatabaseCryptoUtils doesDatabaseNeedToBeConverted:databaseFilePath]);
__block NSData *_Nullable databaseSalt = nil; __block NSData *_Nullable databaseSalt = nil;
YapDatabaseSaltBlock saltBlock = ^(NSData *saltData) { __block NSData *_Nullable databaseKeySpec = nil;
YapRecordDatabaseSaltBlock recordSaltBlock = ^(NSData *saltData) {
OWSAssert(!databaseSalt); OWSAssert(!databaseSalt);
OWSAssert(saltData); OWSAssert(saltData);
databaseSalt = saltData; databaseSalt = saltData;
}; databaseKeySpec = [YapDatabaseCryptoUtils deriveDatabaseKeySpecForPassword:databasePassword saltData:saltData];
__block NSData *_Nullable databaseKeySpec = nil; XCTAssert(databaseKeySpec.length == kSQLCipherKeySpecLength);
YapDatabaseKeySpecBlock keySpecBlock = ^(NSData *keySpecData) {
OWSAssert(!databaseKeySpec);
OWSAssert(keySpecData);
databaseKeySpec = keySpecData; return YES;
}; };
NSError *_Nullable error = [YapDatabaseCryptoUtils convertDatabaseIfNecessary:databaseFilePath NSError *_Nullable error = [YapDatabaseCryptoUtils convertDatabaseIfNecessary:databaseFilePath
databasePassword:databasePassword databasePassword:databasePassword
saltBlock:saltBlock recordSaltBlock:recordSaltBlock];
keySpecBlock:keySpecBlock];
if (error) { if (error) {
DDLogError(@"%s error: %@", __PRETTY_FUNCTION__, error); DDLogError(@"%s error: %@", __PRETTY_FUNCTION__, error);
} }
@ -340,23 +340,22 @@ NS_ASSUME_NONNULL_BEGIN
XCTAssertTrue([YapDatabaseCryptoUtils doesDatabaseNeedToBeConverted:databaseFilePath]); XCTAssertTrue([YapDatabaseCryptoUtils doesDatabaseNeedToBeConverted:databaseFilePath]);
__block NSData *_Nullable databaseSalt = nil; __block NSData *_Nullable databaseSalt = nil;
YapDatabaseSaltBlock saltBlock = ^(NSData *saltData) {
__block NSData *_Nullable databaseKeySpec = nil;
YapRecordDatabaseSaltBlock recordSaltBlock = ^(NSData *saltData) {
OWSAssert(!databaseSalt); OWSAssert(!databaseSalt);
OWSAssert(saltData); OWSAssert(saltData);
databaseSalt = saltData; databaseSalt = saltData;
}; databaseKeySpec = [YapDatabaseCryptoUtils deriveDatabaseKeySpecForPassword:databasePassword saltData:saltData];
__block NSData *_Nullable databaseKeySpec = nil; XCTAssert(databaseKeySpec.length == kSQLCipherKeySpecLength);
YapDatabaseKeySpecBlock keySpecBlock = ^(NSData *keySpecData) {
OWSAssert(!databaseKeySpec);
OWSAssert(keySpecData);
databaseKeySpec = keySpecData; return YES;
}; };
NSError *_Nullable error = [YapDatabaseCryptoUtils convertDatabaseIfNecessary:databaseFilePath NSError *_Nullable error = [YapDatabaseCryptoUtils convertDatabaseIfNecessary:databaseFilePath
databasePassword:databasePassword databasePassword:databasePassword
saltBlock:saltBlock recordSaltBlock:recordSaltBlock];
keySpecBlock:keySpecBlock];
if (error) { if (error) {
DDLogError(@"%s error: %@", __PRETTY_FUNCTION__, error); DDLogError(@"%s error: %@", __PRETTY_FUNCTION__, error);
} }
@ -374,6 +373,40 @@ NS_ASSUME_NONNULL_BEGIN
XCTAssertTrue(isValid); XCTAssertTrue(isValid);
} }
// If we fail to record the salt for some reason, we'll be unable to re-open the database
// halt the conversion in hopes that either the failure is intermittent or we can push out
// a patch to fix the problem without having lost the user's DB.
- (void)testDatabaseConversionDoesNotProceedWhenRecordingSaltFails
{
NSData *databasePassword = [self randomDatabasePassword];
NSString *_Nullable databaseFilePath = [self createUnconvertedDatabase:databasePassword];
XCTAssertTrue([YapDatabaseCryptoUtils doesDatabaseNeedToBeConverted:databaseFilePath]);
__block NSData *_Nullable databaseSalt = nil;
__block NSData *_Nullable databaseKeySpec = nil;
YapRecordDatabaseSaltBlock recordSaltBlock = ^(NSData *saltData) {
OWSAssert(!databaseSalt);
OWSAssert(saltData);
// Simulate a failure to record the new salt, e.g. if KDF returns nil
return NO;
};
NSError *_Nullable error = [YapDatabaseCryptoUtils convertDatabaseIfNecessary:databaseFilePath
databasePassword:databasePassword
recordSaltBlock:recordSaltBlock];
XCTAssertNotNil(error);
XCTAssertTrue([YapDatabaseCryptoUtils doesDatabaseNeedToBeConverted:databaseFilePath]);
BOOL isValid = [self verifyTestDatabase:databaseFilePath
databasePassword:databasePassword
databaseSalt:nil
databaseKeySpec:databaseKeySpec];
XCTAssertTrue(isValid);
}
// Verifies that legacy users with non-converted databases can convert. // Verifies that legacy users with non-converted databases can convert.
- (void)testDatabaseConversionPerformance_WithKeyspec - (void)testDatabaseConversionPerformance_WithKeyspec
{ {
@ -382,7 +415,7 @@ NS_ASSUME_NONNULL_BEGIN
const int kItemCount = 50 * 1000; const int kItemCount = 50 * 1000;
// Create an populate the unconverted database. // Create and populate an unconverted database.
[self openYapDatabase:databaseFilePath [self openYapDatabase:databaseFilePath
databasePassword:databasePassword databasePassword:databasePassword
databaseSalt:nil databaseSalt:nil
@ -391,7 +424,8 @@ NS_ASSUME_NONNULL_BEGIN
YapDatabaseConnection *dbConnection = database.newConnection; YapDatabaseConnection *dbConnection = database.newConnection;
[dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { [dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
for (int i = 0; i < kItemCount; i++) { for (int i = 0; i < kItemCount; i++) {
[transaction setObject:@(i) forKey:@"test_key_name" inCollection:@"test_collection_name"]; NSString *key = [NSString stringWithFormat:@"key-%d", i];
[transaction setObject:@"test-object" forKey:key inCollection:@"test_collection_name"];
} }
}]; }];
}]; }];
@ -399,23 +433,21 @@ NS_ASSUME_NONNULL_BEGIN
XCTAssertTrue([YapDatabaseCryptoUtils doesDatabaseNeedToBeConverted:databaseFilePath]); XCTAssertTrue([YapDatabaseCryptoUtils doesDatabaseNeedToBeConverted:databaseFilePath]);
__block NSData *_Nullable databaseSalt = nil; __block NSData *_Nullable databaseSalt = nil;
YapDatabaseSaltBlock saltBlock = ^(NSData *saltData) { __block NSData *_Nullable databaseKeySpec = nil;
YapRecordDatabaseSaltBlock recordSaltBlock = ^(NSData *saltData) {
OWSAssert(!databaseSalt); OWSAssert(!databaseSalt);
OWSAssert(saltData); OWSAssert(saltData);
databaseSalt = saltData; databaseSalt = saltData;
}; databaseKeySpec = [YapDatabaseCryptoUtils deriveDatabaseKeySpecForPassword:databasePassword saltData:saltData];
__block NSData *_Nullable databaseKeySpec = nil; XCTAssert(databaseKeySpec.length == kSQLCipherKeySpecLength);
YapDatabaseKeySpecBlock keySpecBlock = ^(NSData *keySpecData) {
OWSAssert(!databaseKeySpec);
OWSAssert(keySpecData);
databaseKeySpec = keySpecData; return YES;
}; };
NSError *_Nullable error = [YapDatabaseCryptoUtils convertDatabaseIfNecessary:databaseFilePath NSError *_Nullable error = [YapDatabaseCryptoUtils convertDatabaseIfNecessary:databaseFilePath
databasePassword:databasePassword databasePassword:databasePassword
saltBlock:saltBlock recordSaltBlock:recordSaltBlock];
keySpecBlock:keySpecBlock];
if (error) { if (error) {
DDLogError(@"%s error: %@", __PRETTY_FUNCTION__, error); DDLogError(@"%s error: %@", __PRETTY_FUNCTION__, error);
} }
@ -429,9 +461,9 @@ NS_ASSUME_NONNULL_BEGIN
// Verify the contents of the unconverted database. // Verify the contents of the unconverted database.
__block BOOL isValid = NO; __block BOOL isValid = NO;
[self openYapDatabase:databaseFilePath [self openYapDatabase:databaseFilePath
databasePassword:databasePassword databasePassword:nil
databaseSalt:nil databaseSalt:nil
databaseKeySpec:nil databaseKeySpec:databaseKeySpec
databaseBlock:^(YapDatabase *database) { databaseBlock:^(YapDatabase *database) {
YapDatabaseConnection *dbConnection = database.newConnection; YapDatabaseConnection *dbConnection = database.newConnection;
isValid = [dbConnection numberOfKeysInCollection:@"test_collection_name"] == kItemCount; isValid = [dbConnection numberOfKeysInCollection:@"test_collection_name"] == kItemCount;
@ -449,21 +481,16 @@ NS_ASSUME_NONNULL_BEGIN
[self createDatabase:databasePassword databaseSalt:databaseSalt databaseKeySpec:databaseKeySpec]; [self createDatabase:databasePassword databaseSalt:databaseSalt databaseKeySpec:databaseKeySpec];
XCTAssertFalse([YapDatabaseCryptoUtils doesDatabaseNeedToBeConverted:databaseFilePath]); XCTAssertFalse([YapDatabaseCryptoUtils doesDatabaseNeedToBeConverted:databaseFilePath]);
YapDatabaseSaltBlock saltBlock = ^(NSData *saltData) { YapRecordDatabaseSaltBlock recordSaltBlock = ^(NSData *saltData) {
OWSAssert(saltData); OWSAssert(saltData);
XCTFail(@"%s No conversion should be necessary", __PRETTY_FUNCTION__); XCTFail(@"%s No conversion should be necessary", __PRETTY_FUNCTION__);
}; return NO;
YapDatabaseKeySpecBlock keySpecBlock = ^(NSData *keySpecData) {
OWSAssert(keySpecData);
XCTFail(@"%s No conversion should be necessary", __PRETTY_FUNCTION__);
}; };
NSError *_Nullable error = [YapDatabaseCryptoUtils convertDatabaseIfNecessary:databaseFilePath NSError *_Nullable error = [YapDatabaseCryptoUtils convertDatabaseIfNecessary:databaseFilePath
databasePassword:databasePassword databasePassword:databasePassword
saltBlock:saltBlock recordSaltBlock:recordSaltBlock];
keySpecBlock:keySpecBlock];
if (error) { if (error) {
DDLogError(@"%s error: %@", __PRETTY_FUNCTION__, error); DDLogError(@"%s error: %@", __PRETTY_FUNCTION__, error);
} }
@ -487,21 +514,16 @@ NS_ASSUME_NONNULL_BEGIN
[self createDatabase:databasePassword databaseSalt:databaseSalt databaseKeySpec:databaseKeySpec]; [self createDatabase:databasePassword databaseSalt:databaseSalt databaseKeySpec:databaseKeySpec];
XCTAssertFalse([YapDatabaseCryptoUtils doesDatabaseNeedToBeConverted:databaseFilePath]); XCTAssertFalse([YapDatabaseCryptoUtils doesDatabaseNeedToBeConverted:databaseFilePath]);
YapDatabaseSaltBlock saltBlock = ^(NSData *saltData) { YapRecordDatabaseSaltBlock recordSaltBlock = ^(NSData *saltData) {
OWSAssert(saltData); OWSAssert(saltData);
XCTFail(@"%s No conversion should be necessary", __PRETTY_FUNCTION__); XCTFail(@"%s No conversion should be necessary", __PRETTY_FUNCTION__);
}; return NO;
YapDatabaseKeySpecBlock keySpecBlock = ^(NSData *keySpecData) {
OWSAssert(keySpecData);
XCTFail(@"%s No conversion should be necessary", __PRETTY_FUNCTION__);
}; };
NSError *_Nullable error = [YapDatabaseCryptoUtils convertDatabaseIfNecessary:databaseFilePath NSError *_Nullable error = [YapDatabaseCryptoUtils convertDatabaseIfNecessary:databaseFilePath
databasePassword:databasePassword databasePassword:databasePassword
saltBlock:saltBlock recordSaltBlock:recordSaltBlock];
keySpecBlock:keySpecBlock];
if (error) { if (error) {
DDLogError(@"%s error: %@", __PRETTY_FUNCTION__, error); DDLogError(@"%s error: %@", __PRETTY_FUNCTION__, error);
} }
@ -929,6 +951,199 @@ NS_ASSUME_NONNULL_BEGIN
DDLogInfo(@"%@: %@", label, output); DDLogInfo(@"%@: %@", label, output);
} }
#pragma mark - keychain strategy benchmarks
- (void)verifyTestDatabase:(NSString *)databaseFilePath
databaseKeySpecBlock:(NSData *_Nullable (^_Nullable)(void))databaseKeySpecBlock
databasePasswordBlock:(NSData *_Nullable (^_Nullable)(void))databasePasswordBlock
databaseSaltBlock:(NSData *_Nullable (^_Nullable)(void))databaseSaltBlock
{
NSData *_Nullable databaseKeySpec = databaseKeySpecBlock ? databaseKeySpecBlock() : nil;
NSData *_Nullable databasePassword = databasePasswordBlock ? databasePasswordBlock() : nil;
NSData *_Nullable databaseSalt = databaseSaltBlock ? databaseSaltBlock() : nil;
[self verifyTestDatabase:databaseFilePath
databasePassword:databasePassword
databaseSalt:databaseSalt
databaseKeySpec:databaseKeySpec];
}
- (void)createTestDatabase:(NSString *)databaseFilePath
databaseKeySpecBlock:(NSData *_Nullable (^_Nullable)(void))databaseKeySpecBlock
databasePasswordBlock:(NSData *_Nullable (^_Nullable)(void))databasePasswordBlock
databaseSaltBlock:(NSData *_Nullable (^_Nullable)(void))databaseSaltBlock
{
NSData *_Nullable databaseKeySpec = databaseKeySpecBlock ? databaseKeySpecBlock() : nil;
NSData *_Nullable databasePassword = databasePasswordBlock ? databasePasswordBlock() : nil;
NSData *_Nullable databaseSalt = databaseSaltBlock ? databaseSaltBlock() : nil;
[self createTestDatabase:databaseFilePath
databasePassword:databasePassword
databaseSalt:databaseSalt
databaseKeySpec:databaseKeySpec];
}
- (void)storeTestPasswordInKeychain:(NSData *)password
{
// legacy password length
OWSAssert(password.length == 30);
[OWSStorage storeKeyChainValue:password keychainKey:@"_OWSTestingPassword"];
}
- (nullable NSData *)fetchTestPasswordFromKeychain
{
NSError *error;
NSData *password = [OWSStorage tryToLoadKeyChainValue:@"_OWSTestingPassword" errorHandle:&error];
OWSAssert(password);
OWSAssert(!error);
// legacy password length
OWSAssert(password.length == 30);
return password;
}
- (void)storeTestSaltInKeychain:(NSData *)salt
{
OWSAssert(salt.length == kSQLCipherSaltLength);
[OWSStorage storeKeyChainValue:salt keychainKey:@"_OWSTestingSalt"];
}
- (nullable NSData *)fetchTestSaltFromKeychain
{
NSError *error;
NSData *salt = [OWSStorage tryToLoadKeyChainValue:@"_OWSTestingSalt" errorHandle:&error];
OWSAssert(salt);
OWSAssert(!error);
OWSAssert(salt.length == kSQLCipherSaltLength);
return salt;
}
- (void)storeTestKeySpecInKeychain:(NSData *)keySpec
{
OWSAssert(keySpec.length == kSQLCipherKeySpecLength);
[OWSStorage storeKeyChainValue:keySpec keychainKey:@"_OWSTestingKeySpec"];
}
- (nullable NSData *)fetchTestKeySpecFromKeychain
{
NSError *error;
NSData *keySpec = [OWSStorage tryToLoadKeyChainValue:@"_OWSTestingKeySpec" errorHandle:&error];
OWSAssert(keySpec);
OWSAssert(!error);
OWSAssert(keySpec.length == kSQLCipherKeySpecLength);
return keySpec;
}
- (void)testWidePassphraseFetchingStrategy
{
NSData *password = [self randomDatabasePassword];
NSData *salt = [self randomDatabaseSalt];
[self measureBlock:^{
NSString *databaseFilePath = [self createTempDatabaseFilePath];
[self createTestDatabase:databaseFilePath
databaseKeySpecBlock:nil
databasePasswordBlock:^() {
return password;
}
databaseSaltBlock:^() {
return salt;
}];
[self verifyTestDatabase:databaseFilePath
databaseKeySpecBlock:nil
databasePasswordBlock:^() {
return password;
}
databaseSaltBlock:^() {
return salt;
}];
}];
}
- (void)testGranularPassphraseFetchingStrategy
{
NSData *password = [self randomDatabasePassword];
NSData *salt = [self randomDatabaseSalt];
[self storeTestPasswordInKeychain:password];
[self storeTestSaltInKeychain:salt];
[self measureBlock:^{
NSString *databaseFilePath = [self createTempDatabaseFilePath];
[self createTestDatabase:databaseFilePath
databaseKeySpecBlock:nil
databasePasswordBlock:^() {
return [self fetchTestPasswordFromKeychain];
}
databaseSaltBlock:^() {
return [self fetchTestSaltFromKeychain];
}];
[self verifyTestDatabase:databaseFilePath
databaseKeySpecBlock:nil
databasePasswordBlock:^() {
return [self fetchTestPasswordFromKeychain];
}
databaseSaltBlock:^() {
return [self fetchTestSaltFromKeychain];
}];
}];
}
- (void)testGranularKeySpecFetchingStrategy
{
NSData *keySpec = [self randomDatabaseKeySpec];
[self storeTestKeySpecInKeychain:keySpec];
[self measureBlock:^{
NSString *databaseFilePath = [self createTempDatabaseFilePath];
[self createTestDatabase:databaseFilePath
databaseKeySpecBlock:^() {
return [self fetchTestKeySpecFromKeychain];
}
databasePasswordBlock:nil
databaseSaltBlock:nil];
[self verifyTestDatabase:databaseFilePath
databaseKeySpecBlock:^() {
return [self fetchTestKeySpecFromKeychain];
}
databasePasswordBlock:nil
databaseSaltBlock:nil];
}];
}
- (void)testWideKeyFetchingStrategy
{
NSData *keySpec = [self randomDatabaseKeySpec];
[self measureBlock:^{
NSString *databaseFilePath = [self createTempDatabaseFilePath];
[self createTestDatabase:databaseFilePath
databaseKeySpecBlock:^() {
return keySpec;
}
databasePasswordBlock:nil
databaseSaltBlock:nil];
[self verifyTestDatabase:databaseFilePath
databaseKeySpecBlock:^() {
return keySpec;
}
databasePasswordBlock:nil
databaseSaltBlock:nil];
}];
}
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

@ -1215,7 +1215,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
thread:message.thread thread:message.thread
attempts:OWSMessageSenderRetryAttempts attempts:OWSMessageSenderRetryAttempts
success:^{ success:^{
DDLogInfo(@"Succesfully sent sync transcript."); DDLogInfo(@"Successfully sent sync transcript.");
} }
failure:^(NSError *error) { failure:^(NSError *error) {
// FIXME: We don't yet honor the isRetryable flag here, since sendSyncTranscriptForMessage // FIXME: We don't yet honor the isRetryable flag here, since sendSyncTranscriptForMessage

@ -75,13 +75,11 @@ extern NSString *const StorageIsReadyNotification;
*/ */
+ (BOOL)isDatabasePasswordAccessible; + (BOOL)isDatabasePasswordAccessible;
+ (nullable NSData *)tryToLoadDatabasePassword:(NSError **)errorHandle; + (nullable NSData *)tryToLoadDatabaseLegacyPassphrase:(NSError **)errorHandle;
+ (void)removeLegacyPassphrase;
+ (nullable NSData *)tryToLoadDatabaseSalt:(NSError **)errorHandle; + (void)storeDatabaseCipherKeySpec:(NSData *)cipherKeySpecData;
+ (void)storeDatabaseSalt:(NSData *)saltData;
+ (nullable NSData *)tryToLoadDatabaseKeySpec:(NSError **)errorHandle;
+ (void)storeDatabaseKeySpec:(NSData *)keySpecData;
@end @end

@ -27,9 +27,8 @@ NSString *const OWSStorageExceptionName_NoDatabase = @"OWSStorageExceptionName_N
NSString *const OWSResetStorageNotification = @"OWSResetStorageNotification"; NSString *const OWSResetStorageNotification = @"OWSResetStorageNotification";
static NSString *keychainService = @"TSKeyChainService"; static NSString *keychainService = @"TSKeyChainService";
static NSString *keychainDBPassAccount = @"TSDatabasePass"; static NSString *keychainDBLegacyPassphrase = @"TSDatabasePass";
static NSString *keychainDBSalt = @"OWSDatabaseSalt"; static NSString *keychainDBCipherKeySpec = @"OWSDatabaseCipherKeySpec";
static NSString *keychainDBKeySpec = @"OWSDatabaseKeySpec";
const NSUInteger kDatabasePasswordLength = 30; const NSUInteger kDatabasePasswordLength = 30;
@ -381,26 +380,22 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void);
- (BOOL)tryToLoadDatabase - (BOOL)tryToLoadDatabase
{ {
// We determine the database password, salt and key spec first, since a side effect of
// this can be deleting any existing database file (if we're recovering
// from a corrupt keychain).
//
// 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.
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]; YapDatabaseOptions *options = [[YapDatabaseOptions alloc] init];
options.corruptAction = YapDatabaseCorruptAction_Fail; options.corruptAction = YapDatabaseCorruptAction_Fail;
options.enableMultiProcessSupport = YES; options.enableMultiProcessSupport = YES;
options.cipherKeySpecBlock = ^{ options.cipherKeySpecBlock = ^{
// Rather than compute this once and capture the value of the key
// in the closure, we prefer to fetch the key from the keychain multiple times
// in order to keep the key out of application memory.
NSData *databaseKeySpec = [self databaseKeySpec];
OWSAssert(databaseKeySpec.length == kSQLCipherKeySpecLength);
return databaseKeySpec; return databaseKeySpec;
}; };
// We leave a portion of the header decrypted so that iOS will recognize the file
// as a SQLite database. Otherwise, because the database lives in a shared data container,
// and our usage of sqlite's write-ahead logging retains a lock on the database, the OS
// would kill the app/share extension as soon as it is backgrounded.
options.cipherUnencryptedHeaderLength = kSqliteHeaderLength; options.cipherUnencryptedHeaderLength = kSqliteHeaderLength;
// If any of these asserts fails, we need to verify and update // If any of these asserts fails, we need to verify and update
@ -412,6 +407,11 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void);
OWSAssert(options.pragmaJournalSizeLimit == 0); OWSAssert(options.pragmaJournalSizeLimit == 0);
OWSAssert(options.pragmaMMapSize == 0); OWSAssert(options.pragmaMMapSize == 0);
// Sanity checking elsewhere asserts we should only regenerate key specs when
// there is no existing database, so rather than lazily generate in the cipherKeySpecBlock
// we must ensure the keyspec exists before we create the database.
[self ensureDatabaseKeySpecExists];
OWSDatabase *database = [[OWSDatabase alloc] initWithPath:[self databaseFilePath] OWSDatabase *database = [[OWSDatabase alloc] initWithPath:[self databaseFilePath]
serializer:nil serializer:nil
deserializer:[[self class] logOnFailureDeserializer] deserializer:[[self class] logOnFailureDeserializer]
@ -506,7 +506,7 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void);
// This might be redundant but in the spirit of thoroughness... // This might be redundant but in the spirit of thoroughness...
[self deleteDatabaseFiles]; [self deleteDatabaseFiles];
[self deletePasswordFromKeychain]; [self deleteDBKeys];
if (CurrentAppContext().isMainApp) { if (CurrentAppContext().isMainApp) {
[TSAttachmentStream deleteAttachments]; [TSAttachmentStream deleteAttachments];
@ -528,124 +528,62 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void);
+ (BOOL)isDatabasePasswordAccessible + (BOOL)isDatabasePasswordAccessible
{ {
[SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
NSError *error; NSError *error;
NSString *dbPassword = [SAMKeychain passwordForService:keychainService account:keychainDBPassAccount error:&error]; NSData *cipherKeySpec = [self tryToLoadDatabaseCipherKeySpec:&error];
if (dbPassword && !error) { if (cipherKeySpec && !error) {
return YES; return YES;
} }
if (error) { if (error) {
DDLogWarn(@"Database password couldn't be accessed: %@", error.localizedDescription); DDLogWarn(@"Database key couldn't be accessed: %@", error.localizedDescription);
} }
return NO; return NO;
} }
+ (nullable NSData *)tryToLoadKeyChainValue:(NSString *)keychainKey errorHandle:(NSError **)errorHandle + (nullable NSData *)tryToLoadDatabaseLegacyPassphrase:(NSError **)errorHandle
{
OWSAssert(keychainKey.length > 0);
OWSAssert(errorHandle);
[SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
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];
}
+ (nullable NSData *)tryToLoadDatabaseKeySpec:(NSError **)errorHandle
{ {
return [self tryToLoadKeyChainValue:keychainDBKeySpec errorHandle:errorHandle]; return [self tryToLoadKeyChainValue:keychainDBLegacyPassphrase errorHandle:errorHandle];
} }
- (NSData *)databasePassword + (nullable NSData *)tryToLoadDatabaseCipherKeySpec:(NSError **)errorHandle
{ {
return [self loadMetadataOrClearDatabase:^(NSError **_Nullable errorHandle) { NSData *_Nullable data = [self tryToLoadKeyChainValue:keychainDBCipherKeySpec errorHandle:errorHandle];
return [OWSStorage tryToLoadDatabasePassword:errorHandle]; OWSAssert(!data || data.length == kSQLCipherKeySpecLength);
}
createDataBlock:^{
NSData *passwordData = [self createAndSetNewDatabasePassword];
NSData *saltData = [self createAndSetNewDatabaseSalt];
NSData *keySpecData = [self createAndSetNewDatabaseKeySpec];
OWSAssert(passwordData.length > 0); return data;
OWSAssert(saltData.length == kSQLCipherSaltLength);
OWSAssert(keySpecData.length == kSQLCipherKeySpecLength);
return passwordData;
}
label:@"Database password"];
} }
- (NSData *)databaseSalt + (void)storeDatabaseCipherKeySpec:(NSData *)cipherKeySpecData
{ {
return [self loadMetadataOrClearDatabase:^(NSError **_Nullable errorHandle) { OWSAssert(cipherKeySpecData.length == kSQLCipherKeySpecLength);
return [OWSStorage tryToLoadDatabaseSalt:errorHandle];
}
createDataBlock:^{
NSData *passwordData = [self createAndSetNewDatabasePassword];
NSData *saltData = [self createAndSetNewDatabaseSalt];
NSData *keySpecData = [self createAndSetNewDatabaseKeySpec];
OWSAssert(passwordData.length > 0); [self storeKeyChainValue:cipherKeySpecData keychainKey:keychainDBCipherKeySpec];
OWSAssert(saltData.length == kSQLCipherSaltLength);
OWSAssert(keySpecData.length == kSQLCipherKeySpecLength);
return saltData;
}
label:@"Database salt"];
} }
- (NSData *)databaseKeySpec + (void)removeLegacyPassphrase
{ {
return [self loadMetadataOrClearDatabase:^(NSError **_Nullable errorHandle) { DDLogInfo(@"%@ removing legacy passphrase", self.logTag);
return [OWSStorage tryToLoadDatabaseKeySpec:errorHandle];
}
createDataBlock:^{
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); [SAMKeychain deletePasswordForService:keychainService account:keychainDBLegacyPassphrase];
OWSAssert(saltData.length == kSQLCipherSaltLength);
OWSAssert(keySpecData.length == kSQLCipherKeySpecLength);
return keySpecData;
}
label:@"Database key spec"];
} }
- (NSData *)loadMetadataOrClearDatabase:(LoadDatabaseMetadataBlock)loadDataBlock - (void)ensureDatabaseKeySpecExists
createDataBlock:(CreateDatabaseMetadataBlock)createDataBlock
label:(NSString *)label
{ {
OWSAssert(loadDataBlock);
OWSAssert(createDataBlock);
NSError *error; NSError *error;
NSData *_Nullable data = loadDataBlock(&error); NSData *_Nullable keySpec = [[self class] tryToLoadDatabaseCipherKeySpec:&error];
if (error) { if (error || (keySpec.length != kSQLCipherKeySpecLength)) {
// Because we use kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, // Because we use kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
// the keychain will be inaccessible after device restart until // the keychain will be inaccessible after device restart until
// device is unlocked for the first time. If the app receives // device is unlocked for the first time. If the app receives
// a push notification, we won't be able to access the keychain to // a push notification, we won't be able to access the keychain to
// process that notification, so we should just terminate by throwing // process that notification, so we should just terminate by throwing
// an uncaught exception. // an uncaught exception.
NSString *errorDescription = NSString *errorDescription = [NSString
[NSString stringWithFormat:@"%@ inaccessible. No unlock since device restart? Error: %@", label, error]; stringWithFormat:@"CipherKeySpec inaccessible. New install or no unlock since device restart? Error: %@",
error];
if (CurrentAppContext().isMainApp) { if (CurrentAppContext().isMainApp) {
UIApplicationState applicationState = CurrentAppContext().mainApplicationState; UIApplicationState applicationState = CurrentAppContext().mainApplicationState;
errorDescription = errorDescription =
@ -659,19 +597,17 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void);
// TODO: Rather than crash here, we should detect the situation earlier // TODO: Rather than crash here, we should detect the situation earlier
// and exit gracefully - (in the app delegate?). See the ` // and exit gracefully - (in the app delegate?). See the `
// This is a last ditch effort to avoid blowing away the user's database. // This is a last ditch effort to avoid blowing away the user's database.
[self backgroundedAppDatabasePasswordInaccessibleWithErrorDescription:errorDescription]; [self raiseKeySpecInaccessibleExceptionWithErrorDescription:errorDescription];
} }
} else { } else {
[self backgroundedAppDatabasePasswordInaccessibleWithErrorDescription: [self raiseKeySpecInaccessibleExceptionWithErrorDescription:@"CipherKeySpec inaccessible; not main app."];
[NSString stringWithFormat:@"%@ inaccessible; not main app.", label]];
} }
// At this point, either this is a new install so there's no existing password to retrieve // 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 // or the keychain has become corrupt. Either way, we want to get back to a
// "known good state" and behave like a new install. // "known good state" and behave like a new install.
BOOL doesDBExist = [NSFileManager.defaultManager fileExistsAtPath:[self databaseFilePath]];
BOOL shouldHaveDatabaseMetadata = [NSFileManager.defaultManager fileExistsAtPath:[self databaseFilePath]]; if (doesDBExist) {
if (shouldHaveDatabaseMetadata) {
OWSFail(@"%@ Could not load database metadata", self.logTag); OWSFail(@"%@ Could not load database metadata", self.logTag);
OWSProdCritical([OWSAnalyticsEvents storageErrorCouldNotLoadDatabaseSecondAttempt]); OWSProdCritical([OWSAnalyticsEvents storageErrorCouldNotLoadDatabaseSecondAttempt]);
} }
@ -679,47 +615,30 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void);
// Try to reset app by deleting database. // Try to reset app by deleting database.
[OWSStorage resetAllStorage]; [OWSStorage resetAllStorage];
data = createDataBlock(); keySpec = [Randomness generateRandomBytes:(int)kSQLCipherKeySpecLength];
[[self class] storeDatabaseCipherKeySpec:keySpec];
} }
return data;
}
- (NSData *)createAndSetNewDatabasePassword
{
NSData *password = [[[Randomness generateRandomBytes:kDatabasePasswordLength] base64EncodedString]
dataUsingEncoding:NSUTF8StringEncoding];
[OWSStorage storeDatabasePassword:password];
return password;
}
- (NSData *)createAndSetNewDatabaseSalt
{
NSData *saltData = [Randomness generateRandomBytes:(int)kSQLCipherSaltLength];
[OWSStorage storeDatabaseSalt:saltData];
return saltData;
} }
- (NSData *)createAndSetNewDatabaseKeySpec - (NSData *)databaseKeySpec
{ {
NSData *databasePassword = [self databasePassword]; NSError *error;
OWSAssert(databasePassword.length > 0); NSData *_Nullable keySpec = [[self class] tryToLoadDatabaseCipherKeySpec:&error];
NSData *databaseSalt = [self databaseSalt];
OWSAssert(databaseSalt.length == kSQLCipherSaltLength);
NSData *keySpecData = [YapDatabaseCryptoUtils databaseKeySpecForPassword:databasePassword saltData:databaseSalt]; if (error) {
OWSAssert(keySpecData.length == kSQLCipherKeySpecLength); DDLogError(@"%@ failed to fetch databaseKeySpec with error: %@", self.logTag, error);
[self raiseKeySpecInaccessibleExceptionWithErrorDescription:@"CipherKeySpec inaccessible"];
}
[OWSStorage storeDatabaseKeySpec:keySpecData]; if (keySpec.length != kSQLCipherKeySpecLength) {
DDLogError(@"%@ keyspec had length: %lu", self.logTag, (unsigned long)keySpec.length);
[self raiseKeySpecInaccessibleExceptionWithErrorDescription:@"CipherKeySpec invalid"];
}
return keySpecData; return keySpec;
} }
- (void)backgroundedAppDatabasePasswordInaccessibleWithErrorDescription:(NSString *)errorDescription - (void)raiseKeySpecInaccessibleExceptionWithErrorDescription:(NSString *)errorDescription
{ {
OWSAssert(CurrentAppContext().isMainApp && CurrentAppContext().isInBackground); OWSAssert(CurrentAppContext().isMainApp && CurrentAppContext().isInBackground);
@ -732,11 +651,10 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void);
OWSRaiseException(OWSStorageExceptionName_DatabasePasswordInaccessibleWhileBackgrounded, @"%@", errorDescription); OWSRaiseException(OWSStorageExceptionName_DatabasePasswordInaccessibleWhileBackgrounded, @"%@", errorDescription);
} }
+ (void)deletePasswordFromKeychain + (void)deleteDBKeys
{ {
[SAMKeychain deletePasswordForService:keychainService account:keychainDBPassAccount]; [SAMKeychain deletePasswordForService:keychainService account:keychainDBLegacyPassphrase];
[SAMKeychain deletePasswordForService:keychainService account:keychainDBSalt]; [SAMKeychain deletePasswordForService:keychainService account:keychainDBCipherKeySpec];
[SAMKeychain deletePasswordForService:keychainService account:keychainDBKeySpec];
} }
- (unsigned long long)databaseFileSize - (unsigned long long)databaseFileSize
@ -744,6 +662,14 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void);
return [OWSFileSystem fileSizeOfPath:self.databaseFilePath].unsignedLongLongValue; return [OWSFileSystem fileSizeOfPath:self.databaseFilePath].unsignedLongLongValue;
} }
+ (nullable NSData *)tryToLoadKeyChainValue:(NSString *)keychainKey errorHandle:(NSError **)errorHandle
{
OWSAssert(keychainKey.length > 0);
OWSAssert(errorHandle);
return [SAMKeychain passwordDataForService:keychainService account:keychainKey error:errorHandle];
}
+ (void)storeKeyChainValue:(NSData *)data keychainKey:(NSString *)keychainKey + (void)storeKeyChainValue:(NSData *)data keychainKey:(NSString *)keychainKey
{ {
OWSAssert(keychainKey.length > 0); OWSAssert(keychainKey.length > 0);
@ -756,37 +682,16 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void);
OWSFail(@"%@ Could not store database metadata", self.logTag); OWSFail(@"%@ Could not store database metadata", self.logTag);
OWSProdCritical([OWSAnalyticsEvents storageErrorCouldNotStoreKeychainValue]); OWSProdCritical([OWSAnalyticsEvents storageErrorCouldNotStoreKeychainValue]);
[OWSStorage deletePasswordFromKeychain];
// Sleep to give analytics events time to be delivered. // Sleep to give analytics events time to be delivered.
[NSThread sleepForTimeInterval:15.0f]; [NSThread sleepForTimeInterval:15.0f];
OWSRaiseException( OWSRaiseException(
OWSStorageExceptionName_DatabasePasswordUnwritable, @"Setting keychain value failed with error: %@", error); OWSStorageExceptionName_DatabasePasswordUnwritable, @"Setting keychain value failed with error: %@", error);
} else { } else {
DDLogWarn(@"Succesfully set new keychain value."); DDLogWarn(@"Successfully set new keychain value.");
} }
} }
+ (void)storeDatabasePassword:(NSData *)passwordData
{
[self storeKeyChainValue:passwordData keychainKey:keychainDBPassAccount];
}
+ (void)storeDatabaseSalt:(NSData *)saltData
{
OWSAssert(saltData.length == kSQLCipherSaltLength);
[self storeKeyChainValue:saltData keychainKey:keychainDBSalt];
}
+ (void)storeDatabaseKeySpec:(NSData *)keySpecData
{
OWSAssert(keySpecData.length == kSQLCipherKeySpecLength);
[self storeKeyChainValue:keySpecData keychainKey:keychainDBKeySpec];
}
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

Loading…
Cancel
Save