From 426c9baa1638a3a7955e62222a118bb391075879 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 31 Jan 2018 09:15:16 -0800 Subject: [PATCH] Key material changes - For new installs, generate raw key-spec rather than derive it - Adapt to separated concerns of the key derivation migration from the unencrypted header migration - Reduce number of places where we delete/generate keying information - Only store relevant keying material // FREEBIE --- Podfile.lock | 10 +- Signal/src/AppDelegate.m | 12 +- Signal/test/util/OWSDatabaseConverterTest.m | 49 ++--- SignalServiceKit/src/Storage/OWSStorage.h | 7 +- SignalServiceKit/src/Storage/OWSStorage.m | 201 ++++---------------- 5 files changed, 66 insertions(+), 213 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index e09dd8b5e..a3c1a7d90 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -141,7 +141,7 @@ DEPENDENCIES: - SocketRocket (from `https://github.com/facebook/SocketRocket.git`) - SQLCipher (from `https://github.com/sqlcipher/sqlcipher.git`, commit `d5c2bec`) - SSZipArchive - - YapDatabase/SQLCipher (from `../YapDatabase`) + - YapDatabase/SQLCipher (from `https://github.com/WhisperSystems/YapDatabase.git`, branch `release/unencryptedHeaders`) - YYImage EXTERNAL SOURCES: @@ -167,7 +167,8 @@ EXTERNAL SOURCES: :commit: d5c2bec :git: https://github.com/sqlcipher/sqlcipher.git YapDatabase: - :path: ../YapDatabase + :branch: release/unencryptedHeaders + :git: https://github.com/WhisperSystems/YapDatabase.git CHECKOUT OPTIONS: AxolotlKit: @@ -191,6 +192,9 @@ CHECKOUT OPTIONS: SQLCipher: :commit: d5c2bec :git: https://github.com/sqlcipher/sqlcipher.git + YapDatabase: + :commit: a88958a8db03a050650a495394e1817e48d99f4b + :git: https://github.com/WhisperSystems/YapDatabase.git SPEC CHECKSUMS: AFNetworking: 5e0e199f73d8626b11e79750991f5d173d1f8b67 @@ -217,6 +221,6 @@ SPEC CHECKSUMS: YapDatabase: 299a32de9d350d37a9ac5b0532609d87d5d2a5de YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54 -PODFILE CHECKSUM: bb32efdc239e2a93d6304d25de33a25dc4cdbab2 +PODFILE CHECKSUM: 0d804514eb2db34b9874b653e543255d8c2f5751 COCOAPODS: 1.3.1 diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 20adce933..fa62472d4 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -257,7 +257,7 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; } NSError *error; - NSData *_Nullable databasePassword = [OWSStorage tryToLoadDatabasePassword:&error]; + NSData *_Nullable databasePassword = [OWSStorage tryToLoadDatabaseLegacyPassphrase:&error]; if (!databasePassword || error) { return (error ?: OWSErrorWithCodeDescription( @@ -266,11 +266,11 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; YapDatabaseSaltBlock recordSaltBlock = ^(NSData *saltData) { DDLogVerbose(@"%@ saltData: %@", self.logTag, saltData.hexadecimalString); - [OWSStorage storeDatabaseSalt:saltData]; - }; - YapDatabaseKeySpecBlock keySpecBlock = ^(NSData *keySpecData) { - DDLogVerbose(@"%@ keySpecData: %@", self.logTag, keySpecData.hexadecimalString); - [OWSStorage storeDatabaseKeySpec:keySpecData]; + + // Derive and store the raw cipher key spec, to avoid the ongoing tax of future KDF + NSData *keySpecData = + [YapDatabaseCryptoUtils deriveDatabaseKeySpecForPassword:databasePassword saltData:saltData]; + [OWSStorage storeDatabaseCipherKeySpec:keySpecData]; }; return [YapDatabaseCryptoUtils convertDatabaseIfNecessary:databaseFilePath diff --git a/Signal/test/util/OWSDatabaseConverterTest.m b/Signal/test/util/OWSDatabaseConverterTest.m index b9a24bfc3..a7ffce43f 100644 --- a/Signal/test/util/OWSDatabaseConverterTest.m +++ b/Signal/test/util/OWSDatabaseConverterTest.m @@ -298,19 +298,15 @@ NS_ASSUME_NONNULL_BEGIN XCTAssertTrue([YapDatabaseCryptoUtils doesDatabaseNeedToBeConverted:databaseFilePath]); __block NSData *_Nullable databaseSalt = nil; + __block NSData *_Nullable databaseKeySpec = nil; YapDatabaseSaltBlock recordSaltBlock = ^(NSData *saltData) { OWSAssert(!databaseSalt); OWSAssert(saltData); databaseSalt = saltData; + databaseKeySpec = [YapDatabaseCryptoUtils deriveDatabaseKeySpecForPassword:databasePassword saltData:saltData]; }; - __block NSData *_Nullable databaseKeySpec = nil; - YapDatabaseKeySpecBlock keySpecBlock = ^(NSData *keySpecData) { - OWSAssert(!databaseKeySpec); - OWSAssert(keySpecData); - databaseKeySpec = keySpecData; - }; NSError *_Nullable error = [YapDatabaseCryptoUtils convertDatabaseIfNecessary:databaseFilePath databasePassword:databasePassword recordSaltBlock:recordSaltBlock]; @@ -339,19 +335,16 @@ NS_ASSUME_NONNULL_BEGIN XCTAssertTrue([YapDatabaseCryptoUtils doesDatabaseNeedToBeConverted:databaseFilePath]); __block NSData *_Nullable databaseSalt = nil; - YapDatabaseSaltBlock saltBlock = ^(NSData *saltData) { + + __block NSData *_Nullable databaseKeySpec = nil; + YapDatabaseSaltBlock recordSaltBlock = ^(NSData *saltData) { OWSAssert(!databaseSalt); OWSAssert(saltData); databaseSalt = saltData; + databaseKeySpec = [YapDatabaseCryptoUtils deriveDatabaseKeySpecForPassword:databasePassword saltData:saltData]; }; - __block NSData *_Nullable databaseKeySpec = nil; - YapDatabaseKeySpecBlock keySpecBlock = ^(NSData *keySpecData) { - OWSAssert(!databaseKeySpec); - OWSAssert(keySpecData); - - databaseKeySpec = keySpecData; - }; + NSError *_Nullable error = [YapDatabaseCryptoUtils convertDatabaseIfNecessary:databaseFilePath databasePassword:databasePassword recordSaltBlock:recordSaltBlock]; @@ -398,23 +391,19 @@ NS_ASSUME_NONNULL_BEGIN XCTAssertTrue([YapDatabaseCryptoUtils doesDatabaseNeedToBeConverted:databaseFilePath]); __block NSData *_Nullable databaseSalt = nil; + __block NSData *_Nullable databaseKeySpec = nil; YapDatabaseSaltBlock recordSaltBlock = ^(NSData *saltData) { OWSAssert(!databaseSalt); OWSAssert(saltData); databaseSalt = saltData; + databaseKeySpec = [YapDatabaseCryptoUtils deriveDatabaseKeySpecForPassword:databasePassword saltData:saltData]; }; - __block NSData *_Nullable databaseKeySpec = nil; - YapDatabaseKeySpecBlock keySpecBlock = ^(NSData *keySpecData) { - OWSAssert(!databaseKeySpec); - OWSAssert(keySpecData); - - databaseKeySpec = keySpecData; - }; + + NSError *_Nullable error = [YapDatabaseCryptoUtils convertDatabaseIfNecessary:databaseFilePath databasePassword:databasePassword - saltBlock:saltBlock - keySpecBlock:keySpecBlock]; + recordSaltBlock:recordSaltBlock]; if (error) { DDLogError(@"%s error: %@", __PRETTY_FUNCTION__, error); } @@ -428,9 +417,9 @@ NS_ASSUME_NONNULL_BEGIN // Verify the contents of the unconverted database. __block BOOL isValid = NO; [self openYapDatabase:databaseFilePath - databasePassword:databasePassword + databasePassword:nil databaseSalt:nil - databaseKeySpec:nil + databaseKeySpec:databaseKeySpec databaseBlock:^(YapDatabase *database) { YapDatabaseConnection *dbConnection = database.newConnection; isValid = [dbConnection numberOfKeysInCollection:@"test_collection_name"] == kItemCount; @@ -453,11 +442,6 @@ NS_ASSUME_NONNULL_BEGIN XCTFail(@"%s No conversion should be necessary", __PRETTY_FUNCTION__); }; - YapDatabaseKeySpecBlock keySpecBlock = ^(NSData *keySpecData) { - OWSAssert(keySpecData); - - XCTFail(@"%s No conversion should be necessary", __PRETTY_FUNCTION__); - }; NSError *_Nullable error = [YapDatabaseCryptoUtils convertDatabaseIfNecessary:databaseFilePath databasePassword:databasePassword @@ -490,11 +474,6 @@ NS_ASSUME_NONNULL_BEGIN XCTFail(@"%s No conversion should be necessary", __PRETTY_FUNCTION__); }; - YapDatabaseKeySpecBlock keySpecBlock = ^(NSData *keySpecData) { - OWSAssert(keySpecData); - - XCTFail(@"%s No conversion should be necessary", __PRETTY_FUNCTION__); - }; NSError *_Nullable error = [YapDatabaseCryptoUtils convertDatabaseIfNecessary:databaseFilePath databasePassword:databasePassword diff --git a/SignalServiceKit/src/Storage/OWSStorage.h b/SignalServiceKit/src/Storage/OWSStorage.h index 9101cc3cc..d9ef80d83 100644 --- a/SignalServiceKit/src/Storage/OWSStorage.h +++ b/SignalServiceKit/src/Storage/OWSStorage.h @@ -75,13 +75,10 @@ extern NSString *const StorageIsReadyNotification; */ + (BOOL)isDatabasePasswordAccessible; -+ (nullable NSData *)tryToLoadDatabasePassword:(NSError **)errorHandle; ++ (nullable NSData *)tryToLoadDatabaseLegacyPassphrase:(NSError **)errorHandle; -+ (nullable NSData *)tryToLoadDatabaseSalt:(NSError **)errorHandle; -+ (void)storeDatabaseSalt:(NSData *)saltData; ++ (void)storeDatabaseCipherKeySpec:(NSData *)cipherKeySpecData; -+ (nullable NSData *)tryToLoadDatabaseKeySpec:(NSError **)errorHandle; -+ (void)storeDatabaseKeySpec:(NSData *)keySpecData; @end diff --git a/SignalServiceKit/src/Storage/OWSStorage.m b/SignalServiceKit/src/Storage/OWSStorage.m index 918a589c4..c444ffe6a 100644 --- a/SignalServiceKit/src/Storage/OWSStorage.m +++ b/SignalServiceKit/src/Storage/OWSStorage.m @@ -27,9 +27,8 @@ NSString *const OWSStorageExceptionName_NoDatabase = @"OWSStorageExceptionName_N NSString *const OWSResetStorageNotification = @"OWSResetStorageNotification"; static NSString *keychainService = @"TSKeyChainService"; -static NSString *keychainDBPassAccount = @"TSDatabasePass"; -static NSString *keychainDBSalt = @"OWSDatabaseSalt"; -static NSString *keychainDBKeySpec = @"OWSDatabaseKeySpec"; +static NSString *keychainDBLegacyPassphrase = @"TSDatabasePass"; +static NSString *keychainDBCipherKeySpec = @"OWSDatabaseCipherKeySpec"; const NSUInteger kDatabasePasswordLength = 30; @@ -381,17 +380,9 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void); - (BOOL)tryToLoadDatabase { - // We determine the database password, salt and key spec first, since a side effect of + // We determine the database 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); @@ -401,6 +392,11 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void); options.cipherKeySpecBlock = ^{ 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; // If any of these asserts fails, we need to verify and update @@ -506,7 +502,7 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void); // This might be redundant but in the spirit of thoroughness... [self deleteDatabaseFiles]; - [self deletePasswordFromKeychain]; + [self deleteDBKeys]; if (CurrentAppContext().isMainApp) { [TSAttachmentStream deleteAttachments]; @@ -528,116 +524,41 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void); + (BOOL)isDatabasePasswordAccessible { - [SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly]; NSError *error; - NSString *dbPassword = [SAMKeychain passwordForService:keychainService account:keychainDBPassAccount error:&error]; + NSData *cipherKeySpec = [self tryToLoadDatabaseCipherKeySpec:&error]; - if (dbPassword && !error) { + if (cipherKeySpec && !error) { return YES; } if (error) { - DDLogWarn(@"Database password couldn't be accessed: %@", error.localizedDescription); + DDLogWarn(@"Database key couldn't be accessed: %@", error.localizedDescription); } 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]; + return [self tryToLoadKeyChainValue:keychainDBLegacyPassphrase errorHandle:errorHandle]; } -+ (nullable NSData *)tryToLoadDatabasePassword:(NSError **)errorHandle ++ (nullable NSData *)tryToLoadDatabaseCipherKeySpec:(NSError **)errorHandle { - return [self tryToLoadKeyChainValue:keychainDBPassAccount errorHandle:errorHandle]; + return [self tryToLoadKeyChainValue:keychainDBCipherKeySpec errorHandle:errorHandle]; } -+ (nullable NSData *)tryToLoadDatabaseSalt:(NSError **)errorHandle ++ (void)storeDatabaseCipherKeySpec:(NSData *)cipherKeySpecData { - return [self tryToLoadKeyChainValue:keychainDBSalt errorHandle:errorHandle]; -} + OWSAssert(cipherKeySpecData.length == kSQLCipherKeySpecLength); -+ (nullable NSData *)tryToLoadDatabaseKeySpec:(NSError **)errorHandle -{ - return [self tryToLoadKeyChainValue:keychainDBKeySpec errorHandle:errorHandle]; -} - -- (NSData *)databasePassword -{ - return [self loadMetadataOrClearDatabase:^(NSError **_Nullable errorHandle) { - return [OWSStorage tryToLoadDatabasePassword:errorHandle]; - } - createDataBlock:^{ - NSData *passwordData = [self createAndSetNewDatabasePassword]; - NSData *saltData = [self createAndSetNewDatabaseSalt]; - NSData *keySpecData = [self createAndSetNewDatabaseKeySpec]; - - OWSAssert(passwordData.length > 0); - OWSAssert(saltData.length == kSQLCipherSaltLength); - OWSAssert(keySpecData.length == kSQLCipherKeySpecLength); - - return passwordData; - } - label:@"Database password"]; -} - -- (NSData *)databaseSalt -{ - return [self loadMetadataOrClearDatabase:^(NSError **_Nullable errorHandle) { - return [OWSStorage tryToLoadDatabaseSalt:errorHandle]; - } - createDataBlock:^{ - NSData *passwordData = [self createAndSetNewDatabasePassword]; - NSData *saltData = [self createAndSetNewDatabaseSalt]; - NSData *keySpecData = [self createAndSetNewDatabaseKeySpec]; - - OWSAssert(passwordData.length > 0); - OWSAssert(saltData.length == kSQLCipherSaltLength); - OWSAssert(keySpecData.length == kSQLCipherKeySpecLength); - - return saltData; - } - label:@"Database salt"]; + [self storeKeyChainValue:cipherKeySpecData keychainKey:keychainDBCipherKeySpec]; } - (NSData *)databaseKeySpec { - // Get or generate salt and cipherKeyData - - return [self loadMetadataOrClearDatabase:^(NSError **_Nullable errorHandle) { - 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); - OWSAssert(saltData.length == kSQLCipherSaltLength); - OWSAssert(keySpecData.length == kSQLCipherKeySpecLength); - - return keySpecData; - } - label:@"Database key spec"]; -} - -- (NSData *)loadMetadataOrClearDatabase:(LoadDatabaseMetadataBlock)loadDataBlock - createDataBlock:(CreateDatabaseMetadataBlock)createDataBlock - label:(NSString *)label -{ - OWSAssert(loadDataBlock); - OWSAssert(createDataBlock); - NSError *error; - NSData *_Nullable data = loadDataBlock(&error); + NSData *_Nullable data = [[self class] tryToLoadDatabaseCipherKeySpec:&error]; if (error) { // Because we use kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, @@ -647,7 +568,7 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void); // process that notification, so we should just terminate by throwing // an uncaught exception. NSString *errorDescription = - [NSString stringWithFormat:@"%@ inaccessible. No unlock since device restart? Error: %@", label, error]; + [NSString stringWithFormat:@"CipherKeySpec inaccessible. No unlock since device restart? Error: %@", error]; if (CurrentAppContext().isMainApp) { UIApplicationState applicationState = CurrentAppContext().mainApplicationState; errorDescription = @@ -661,11 +582,10 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void); // TODO: Rather than crash here, we should detect the situation earlier // and exit gracefully - (in the app delegate?). See the ` // This is a last ditch effort to avoid blowing away the user's database. - [self backgroundedAppDatabasePasswordInaccessibleWithErrorDescription:errorDescription]; + [self raiseKeySpecInaccessibleExceptionWithErrorDescription:errorDescription]; } } else { - [self backgroundedAppDatabasePasswordInaccessibleWithErrorDescription: - [NSString stringWithFormat:@"%@ inaccessible; not main app.", label]]; + [self raiseKeySpecInaccessibleExceptionWithErrorDescription:@"CipherKeySpec inaccessible; not main app."]; } // At this point, either this is a new install so there's no existing password to retrieve @@ -681,47 +601,14 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void); // Try to reset app by deleting database. [OWSStorage resetAllStorage]; - data = createDataBlock(); + data = [Randomness generateRandomBytes:(int)kSQLCipherKeySpecLength]; + [[self class] storeDatabaseCipherKeySpec:data]; } 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 *databasePassword = [self databasePassword]; - OWSAssert(databasePassword.length > 0); - NSData *databaseSalt = [self databaseSalt]; - OWSAssert(databaseSalt.length == kSQLCipherSaltLength); - - NSData *keySpecData = [YapDatabaseCryptoUtils databaseKeySpecForPassword:databasePassword saltData:databaseSalt]; - OWSAssert(keySpecData.length == kSQLCipherKeySpecLength); - - [OWSStorage storeDatabaseKeySpec:keySpecData]; - - return keySpecData; -} - -- (void)backgroundedAppDatabasePasswordInaccessibleWithErrorDescription:(NSString *)errorDescription +- (void)raiseKeySpecInaccessibleExceptionWithErrorDescription:(NSString *)errorDescription { OWSAssert(CurrentAppContext().isMainApp && CurrentAppContext().isInBackground); @@ -734,11 +621,10 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void); OWSRaiseException(OWSStorageExceptionName_DatabasePasswordInaccessibleWhileBackgrounded, @"%@", errorDescription); } -+ (void)deletePasswordFromKeychain ++ (void)deleteDBKeys { - [SAMKeychain deletePasswordForService:keychainService account:keychainDBPassAccount]; - [SAMKeychain deletePasswordForService:keychainService account:keychainDBSalt]; - [SAMKeychain deletePasswordForService:keychainService account:keychainDBKeySpec]; + [SAMKeychain deletePasswordForService:keychainService account:keychainDBLegacyPassphrase]; + [SAMKeychain deletePasswordForService:keychainService account:keychainDBCipherKeySpec]; } - (unsigned long long)databaseFileSize @@ -746,6 +632,14 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void); 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 { OWSAssert(keychainKey.length > 0); @@ -758,8 +652,6 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void); OWSFail(@"%@ Could not store database metadata", self.logTag); OWSProdCritical([OWSAnalyticsEvents storageErrorCouldNotStoreKeychainValue]); - [OWSStorage deletePasswordFromKeychain]; - // Sleep to give analytics events time to be delivered. [NSThread sleepForTimeInterval:15.0f]; @@ -770,25 +662,6 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void); } } -+ (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 NS_ASSUME_NONNULL_END