2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
24 #import "KeychainXCTest.h"
25 #import "SecDbKeychainItem.h"
26 #import "SecdTestKeychainUtilities.h"
28 #import "SecDbKeychainItemV7.h"
29 #import "SecItemPriv.h"
30 #import "SecItemServer.h"
32 #import "SecDbKeychainSerializedItemV7.h"
33 #import "SecDbKeychainSerializedMetadata.h"
34 #import "SecDbKeychainSerializedSecretData.h"
35 #import "SecDbKeychainSerializedAKSWrappedKey.h"
36 #import <utilities/SecCFWrappers.h>
37 #import <SecurityFoundation/SFEncryptionOperation.h>
38 #import <SecurityFoundation/SFCryptoServicesErrors.h>
39 #import <XCTest/XCTest.h>
40 #import <OCMock/OCMock.h>
43 @interface SecDbKeychainItemV7 ()
45 + (SFAESKeySpecifier*)keySpecifier;
52 @interface KeychainCryptoTests : KeychainXCTest
55 @implementation KeychainCryptoTests
57 static keyclass_t parse_keyclass(CFTypeRef value) {
58 if (!value || CFGetTypeID(value) != CFStringGetTypeID()) {
62 if (CFEqual(value, kSecAttrAccessibleWhenUnlocked)) {
65 else if (CFEqual(value, kSecAttrAccessibleAfterFirstUnlock)) {
68 else if (CFEqual(value, kSecAttrAccessibleAlwaysPrivate)) {
71 else if (CFEqual(value, kSecAttrAccessibleWhenUnlockedThisDeviceOnly)) {
74 else if (CFEqual(value, kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)) {
77 else if (CFEqual(value, kSecAttrAccessibleAlwaysThisDeviceOnlyPrivate)) {
80 else if (CFEqual(value, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly)) {
81 return key_class_akpu;
88 - (NSDictionary* _Nullable)addTestItemExpecting:(OSStatus)code account:(NSString*)account accessible:(NSString*)accessible
90 NSDictionary* addQuery = @{ (id)kSecClass : (id)kSecClassGenericPassword,
91 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
92 (id)kSecAttrAccount : account,
93 (id)kSecAttrService : [NSString stringWithFormat:@"%@-Service", account],
94 (id)kSecAttrAccessible : (id)accessible,
95 (id)kSecAttrNoLegacy : @(YES),
96 (id)kSecReturnAttributes : @(YES),
98 CFTypeRef result = NULL;
100 if(code == errSecSuccess) {
101 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)addQuery, &result), code, @"Should have succeeded in adding test item to keychain");
102 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd");
104 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)addQuery, &result), code, @"Should have failed to adding test item to keychain with code %d", code);
105 XCTAssertNil((__bridge id)result, @"Should not have received a dictionary back from SecItemAdd");
108 return CFBridgingRelease(result);
111 - (NSDictionary* _Nullable)findTestItemExpecting:(OSStatus)code account:(NSString*)account
113 NSDictionary* findQuery = @{ (id)kSecClass : (id)kSecClassGenericPassword,
114 (id)kSecAttrAccount : account,
115 (id)kSecAttrService : [NSString stringWithFormat:@"%@-Service", account],
116 (id)kSecAttrNoLegacy : @(YES),
117 (id)kSecReturnAttributes : @(YES),
119 CFTypeRef result = NULL;
121 if(code == errSecSuccess) {
122 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), code, @"Should have succeeded in finding test tiem");
123 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemCopyMatching");
125 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), code, @"Should have failed to find items in keychain with code %d", code);
126 XCTAssertNotNil((__bridge id)result, @"Should not have received a dictionary back from SecItemCopyMatching");
129 return CFBridgingRelease(result);
133 - (void)testBasicEncryptDecrypt
135 CFDataRef enc = NULL;
136 CFErrorRef error = NULL;
137 SecAccessControlRef ac = NULL;
139 NSDictionary* secretData = @{(id)kSecValueData : @"secret here"};
141 ac = SecAccessControlCreate(NULL, &error);
142 XCTAssertNotNil((__bridge id)ac, @"failed to create access control with error: %@", (__bridge id)error);
143 XCTAssertNil((__bridge id)error, @"encountered error attempting to create access control: %@", (__bridge id)error);
144 XCTAssertTrue(SecAccessControlSetProtection(ac, kSecAttrAccessibleWhenUnlocked, &error), @"failed to set access control protection with error: %@", error);
145 XCTAssertNil((__bridge id)error, @"encountered error attempting to set access control protection: %@", (__bridge id)error);
147 XCTAssertTrue(ks_encrypt_data(KEYBAG_DEVICE, ac, NULL, (__bridge CFDictionaryRef)secretData, (__bridge CFDictionaryRef)@{}, NULL, &enc, true, &error), @"failed to encrypt data with error: %@", error);
148 XCTAssertTrue(enc != NULL, @"failed to get encrypted data from encryption function");
149 XCTAssertNil((__bridge id)error, @"encountered error attempting to encrypt data: %@", (__bridge id)error);
152 CFMutableDictionaryRef attributes = NULL;
153 uint32_t version = 0;
155 keyclass_t keyclass = 0;
156 XCTAssertTrue(ks_decrypt_data(KEYBAG_DEVICE, kAKSKeyOpDecrypt, &ac, NULL, enc, NULL, NULL, &attributes, &version, true, &keyclass, &error), @"failed to decrypt data with error: %@", error);
157 XCTAssertNil((__bridge id)error, @"encountered error attempting to decrypt data: %@", (__bridge id)error);
158 XCTAssertEqual(keyclass, key_class_ak, @"failed to get back the keyclass from decryption");
160 CFTypeRef aclProtection = ac ? SecAccessControlGetProtection(ac) : NULL;
161 XCTAssertNotNil((__bridge id)aclProtection, @"failed to get ACL from keychain item decryption");
163 XCTAssertTrue(CFEqual(aclProtection, kSecAttrAccessibleWhenUnlocked), @"the acl we got back from decryption does not match what we put in");
167 CFReleaseNull(error);
171 - (void)testGetMetadataThenData
173 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
174 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
175 (id)kSecAttrAccount : @"TestAccount",
176 (id)kSecAttrService : @"TestService",
177 (id)kSecAttrNoLegacy : @(YES) };
179 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
180 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
182 NSMutableDictionary* metadataQuery = item.mutableCopy;
183 [metadataQuery removeObjectForKey:(id)kSecValueData];
184 metadataQuery[(id)kSecReturnAttributes] = @(YES);
185 CFTypeRef foundItem = NULL;
186 result = SecItemCopyMatching((__bridge CFDictionaryRef)metadataQuery, &foundItem);
187 XCTAssertEqual(result, 0, @"failed to find the metadata for the item we just added in the keychain");
189 NSMutableDictionary* dataQuery = [(__bridge NSDictionary*)foundItem mutableCopy];
190 dataQuery[(id)kSecReturnData] = @(YES);
191 dataQuery[(id)kSecClass] = (id)kSecClassGenericPassword;
192 dataQuery[(id)kSecAttrNoLegacy] = @(YES);
193 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
194 XCTAssertEqual(result, 0, @"failed to find the data for the item we just added to the keychain");
196 NSData* foundData = (__bridge NSData*)foundItem;
197 if ([foundData isKindOfClass:[NSData class]]) {
198 NSString* foundPassword = [[NSString alloc] initWithData:(__bridge NSData*)foundItem encoding:NSUTF8StringEncoding];
199 XCTAssertEqualObjects(foundPassword, @"password", @"found password (%@) does not match the expected password", foundPassword);
202 XCTAssertTrue(false, @"found data is not the expected class: %@", foundData);
206 - (void)testGetReference
208 NSDictionary* keyParams = @{ (id)kSecAttrKeyType : (id)kSecAttrKeyTypeRSA, (id)kSecAttrKeySizeInBits : @(1024) };
209 SecKeyRef key = SecKeyCreateRandomKey((__bridge CFDictionaryRef)keyParams, NULL);
210 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassKey,
211 (id)kSecValueRef : (__bridge id)key,
212 (id)kSecAttrLabel : @"TestLabel",
213 (id)kSecAttrNoLegacy : @(YES) };
215 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
216 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
218 NSMutableDictionary* refQuery = item.mutableCopy;
219 [refQuery removeObjectForKey:(id)kSecValueData];
220 refQuery[(id)kSecReturnRef] = @(YES);
221 CFTypeRef foundItem = NULL;
222 result = SecItemCopyMatching((__bridge CFDictionaryRef)refQuery, &foundItem);
223 XCTAssertEqual(result, 0, @"failed to find the reference for the item we just added in the keychain");
225 NSData* originalKeyData = (__bridge_transfer NSData*)SecKeyCopyExternalRepresentation(key, NULL);
226 NSData* foundKeyData = (__bridge_transfer NSData*)SecKeyCopyExternalRepresentation((SecKeyRef)foundItem, NULL);
227 XCTAssertEqualObjects(originalKeyData, foundKeyData, @"found key does not match the key we put in the keychain");
230 - (void)testMetadataQueriesDoNotGetSecret
232 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
233 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
234 (id)kSecAttrAccount : @"TestAccount",
235 (id)kSecAttrService : @"TestService",
236 (id)kSecAttrNoLegacy : @(YES) };
238 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
239 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
241 NSMutableDictionary* metadataQuery = item.mutableCopy;
242 [metadataQuery removeObjectForKey:(id)kSecValueData];
243 metadataQuery[(id)kSecReturnAttributes] = @(YES);
244 CFTypeRef foundItem = NULL;
245 result = SecItemCopyMatching((__bridge CFDictionaryRef)metadataQuery, &foundItem);
246 XCTAssertEqual(result, 0, @"failed to find the metadata for the item we just added in the keychain");
248 NSData* data = [(__bridge NSDictionary*)foundItem valueForKey:(id)kSecValueData];
249 XCTAssertNil(data, @"unexpectedly found data in a metadata query");
252 - (void)testDeleteItem
254 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
255 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
256 (id)kSecAttrAccount : @"TestAccount",
257 (id)kSecAttrService : @"TestService",
258 (id)kSecAttrNoLegacy : @(YES) };
260 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
261 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
263 NSMutableDictionary* dataQuery = item.mutableCopy;
264 [dataQuery removeObjectForKey:(id)kSecValueData];
265 dataQuery[(id)kSecReturnData] = @(YES);
266 CFTypeRef foundItem = NULL;
267 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
268 XCTAssertEqual(result, 0, @"failed to find the data for the item we just added in the keychain");
270 result = SecItemDelete((__bridge CFDictionaryRef)dataQuery);
271 XCTAssertEqual(result, 0, @"failed to delete item");
274 - (SecDbKeychainSerializedItemV7*)serializedItemWithPassword:(NSString*)password metadataAttributes:(NSDictionary*)metadata
276 SecDbKeychainItemV7* item = [[SecDbKeychainItemV7 alloc] initWithSecretAttributes:@{(id)kSecValueData : password} metadataAttributes:metadata tamperCheck:[[NSUUID UUID] UUIDString] keyclass:9];
277 [item encryptMetadataWithKeybag:0 error:nil];
278 [item encryptSecretDataWithKeybag:0 accessControl:SecAccessControlCreate(NULL, NULL) acmContext:nil error:nil];
279 SecDbKeychainSerializedItemV7* serializedItem = [[SecDbKeychainSerializedItemV7 alloc] init];
280 serializedItem.encryptedMetadata = item.encryptedMetadataBlob;
281 serializedItem.encryptedSecretData = item.encryptedSecretDataBlob;
282 serializedItem.keyclass = 9;
283 return serializedItem;
286 - (void)testTamperChecksThwartTampering
288 SecDbKeychainSerializedItemV7* serializedItem1 = [self serializedItemWithPassword:@"first password" metadataAttributes:nil];
289 SecDbKeychainSerializedItemV7* serializedItem2 = [self serializedItemWithPassword:@"second password" metadataAttributes:nil];
291 serializedItem1.encryptedSecretData = serializedItem2.encryptedSecretData;
292 NSData* tamperedSerializedItemBlob = serializedItem1.data;
294 NSError* error = nil;
295 SecDbKeychainItemV7* item = [[SecDbKeychainItemV7 alloc] initWithData:tamperedSerializedItemBlob decryptionKeybag:0 error:&error];
296 XCTAssertNil(item, @"unexpectedly deserialized an item blob which has been tampered");
297 XCTAssertNotNil(error, @"failed to get an error when deserializing tampered item blob");
300 - (void)testCacheExpiration
303 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
304 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
305 (id)kSecAttrAccount : @"TestAccount",
306 (id)kSecAttrService : @"TestService",
307 (id)kSecAttrAccessible : (id)kSecAttrAccessibleWhenUnlocked,
308 (id)kSecAttrNoLegacy : @YES };
310 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
311 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
313 NSMutableDictionary* dataQuery = item.mutableCopy;
314 [dataQuery removeObjectForKey:(id)kSecValueData];
315 dataQuery[(id)kSecReturnData] = @(YES);
317 CFTypeRef foundItem = NULL;
319 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
320 XCTAssertEqual(result, 0, @"failed to find the data for the item we just added in the keychain");
321 CFReleaseNull(foundItem);
323 self.lockState = LockStateLockedAndDisallowAKS;
325 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
326 XCTAssertEqual(result, errSecInteractionNotAllowed, @"get the lock error");
327 XCTAssertEqual(foundItem, NULL, @"got item anyway: %@", foundItem);
329 self.lockState = LockStateUnlocked;
331 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
332 XCTAssertEqual(result, 0, @"failed to find the data for the item we just added in the keychain");
333 CFReleaseNull(foundItem);
335 result = SecItemDelete((__bridge CFDictionaryRef)dataQuery);
336 XCTAssertEqual(result, 0, @"failed to delete item");
339 - (void)trashMetadataClassAKey
341 CFErrorRef cferror = NULL;
343 kc_with_dbt(true, &cferror, ^bool(SecDbConnectionRef dbt) {
344 CFErrorRef errref = NULL;
345 SecDbExec(dbt, CFSTR("DELETE FROM metadatakeys WHERE keyclass = '6'"), &errref);
346 XCTAssertEqual(errref, NULL, "Should be no error deleting class A metadatakey");
347 CFReleaseNull(errref);
350 CFReleaseNull(cferror);
352 [[SecDbKeychainMetadataKeyStore sharedStore] dropClassAKeys];
355 - (void)checkDatabaseExistenceOfMetadataKey:(keyclass_t)keyclass shouldExist:(bool)shouldExist
357 CFErrorRef cferror = NULL;
359 kc_with_dbt(true, &cferror, ^bool(SecDbConnectionRef dbt) {
360 __block CFErrorRef errref = NULL;
362 NSString* sql = [NSString stringWithFormat:@"SELECT data, actualKeyclass FROM metadatakeys WHERE keyclass = %d", keyclass];
363 __block bool ok = true;
364 __block bool keyExists = false;
365 ok &= SecDbPrepare(dbt, (__bridge CFStringRef)sql, &errref, ^(sqlite3_stmt *stmt) {
366 ok &= SecDbStep(dbt, stmt, &errref, ^(bool *stop) {
367 NSData* wrappedKeyData = [[NSData alloc] initWithBytes:sqlite3_column_blob(stmt, 0) length:sqlite3_column_bytes(stmt, 0)];
368 NSMutableData* unwrappedKeyData = [NSMutableData dataWithLength:wrappedKeyData.length];
370 keyExists = !!unwrappedKeyData;
374 XCTAssertTrue(ok, "Should have completed all operations correctly");
375 XCTAssertEqual(errref, NULL, "Should be no error deleting class A metadatakey");
378 XCTAssertTrue(keyExists, "Metadata class key should exist");
380 XCTAssertFalse(keyExists, "Metadata class key should not exist");
382 CFReleaseNull(errref);
385 CFReleaseNull(cferror);
388 - (void)testKeychainCorruptionCopyMatching
390 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
391 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
392 (id)kSecAttrAccount : @"TestAccount",
393 (id)kSecAttrService : @"TestService",
394 (id)kSecAttrAccessible : (id)kSecAttrAccessibleWhenUnlocked,
395 (id)kSecAttrNoLegacy : @YES };
397 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
398 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
399 [self checkDatabaseExistenceOfMetadataKey:key_class_ak shouldExist:true];
401 NSMutableDictionary* dataQuery = item.mutableCopy;
402 [dataQuery removeObjectForKey:(id)kSecValueData];
403 dataQuery[(id)kSecReturnData] = @(YES);
405 CFTypeRef foundItem = NULL;
407 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
408 XCTAssertEqual(result, 0, @"failed to find the data for the item we just added in the keychain");
409 CFReleaseNull(foundItem);
411 [self trashMetadataClassAKey];
412 [self checkDatabaseExistenceOfMetadataKey:key_class_ak shouldExist:false];
414 /* when metadata corrupted, we should not find the item */
415 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
416 XCTAssertEqual(result, errSecItemNotFound, @"failed to find the data for the item we just added in the keychain");
417 CFReleaseNull(foundItem);
419 // Just calling SecItemCopyMatching shouldn't have created a new metdata key
420 [self checkDatabaseExistenceOfMetadataKey:key_class_ak shouldExist:false];
422 /* semantics are odd, we should be able to delete it */
423 result = SecItemDelete((__bridge CFDictionaryRef)dataQuery);
424 XCTAssertEqual(result, 0, @"failed to delete item");
427 - (void)testKeychainCorruptionAddOverCorruptedEntry
429 CFTypeRef foundItem = NULL;
430 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
431 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
432 (id)kSecAttrAccount : @"TestAccount",
433 (id)kSecAttrService : @"TestService",
434 (id)kSecAttrAccessible : (id)kSecAttrAccessibleWhenUnlocked,
435 (id)kSecAttrNoLegacy : @YES };
437 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
438 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
440 NSMutableDictionary* dataQuery = item.mutableCopy;
441 [dataQuery removeObjectForKey:(id)kSecValueData];
442 dataQuery[(id)kSecReturnData] = @(YES);
444 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
445 XCTAssertEqual(result, 0, @"failed to find the data for the item we just added in the keychain");
446 CFReleaseNull(foundItem);
448 [self trashMetadataClassAKey];
450 result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
451 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
453 result = SecItemDelete((__bridge CFDictionaryRef)dataQuery);
454 XCTAssertEqual(result, 0, @"failed to delete item");
457 - (void)testKeychainCorruptionUpdateCorruptedEntry
459 CFTypeRef foundItem = NULL;
460 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
461 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
462 (id)kSecAttrAccount : @"TestAccount",
463 (id)kSecAttrService : @"TestService",
464 (id)kSecAttrAccessible : (id)kSecAttrAccessibleWhenUnlocked,
465 (id)kSecAttrNoLegacy : @YES };
467 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
468 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
470 NSMutableDictionary* dataQuery = item.mutableCopy;
471 [dataQuery removeObjectForKey:(id)kSecValueData];
472 dataQuery[(id)kSecReturnData] = @(YES);
474 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
475 XCTAssertEqual(result, 0, @"failed to find the data for the item we just added in the keychain");
476 CFReleaseNull(foundItem);
478 [self trashMetadataClassAKey];
480 NSMutableDictionary* updateQuery = item.mutableCopy;
481 updateQuery[(id)kSecValueData] = NULL;
482 NSDictionary *updateData = @{
483 (id)kSecValueData : [@"foo" dataUsingEncoding:NSUTF8StringEncoding],
486 result = SecItemUpdate((__bridge CFDictionaryRef)updateQuery,
487 (__bridge CFDictionaryRef)updateData );
488 XCTAssertEqual(result, errSecItemNotFound, @"failed to add test item to keychain");
490 result = SecItemDelete((__bridge CFDictionaryRef)dataQuery);
491 XCTAssertEqual(result, 0, @"failed to delete item");
494 - (id)encryptionOperation
499 - (void)testNoCrashWhenMetadataDecryptionFails
501 CFDataRef enc = NULL;
502 CFErrorRef error = NULL;
503 SecAccessControlRef ac = NULL;
505 self.allowDecryption = NO;
507 NSDictionary* secretData = @{(id)kSecValueData : @"secret here"};
509 ac = SecAccessControlCreate(NULL, &error);
510 XCTAssertNotNil((__bridge id)ac, @"failed to create access control with error: %@", (__bridge id)error);
511 XCTAssertNil((__bridge id)error, @"encountered error attempting to create access control: %@", (__bridge id)error);
512 XCTAssertTrue(SecAccessControlSetProtection(ac, kSecAttrAccessibleWhenUnlocked, &error), @"failed to set access control protection with error: %@", error);
513 XCTAssertNil((__bridge id)error, @"encountered error attempting to set access control protection: %@", (__bridge id)error);
515 XCTAssertTrue(ks_encrypt_data(KEYBAG_DEVICE, ac, NULL, (__bridge CFDictionaryRef)secretData, (__bridge CFDictionaryRef)@{}, NULL, &enc, true, &error), @"failed to encrypt data with error: %@", error);
516 XCTAssertTrue(enc != NULL, @"failed to get encrypted data from encryption function");
517 XCTAssertNil((__bridge id)error, @"encountered error attempting to encrypt data: %@", (__bridge id)error);
520 CFMutableDictionaryRef attributes = NULL;
521 uint32_t version = 0;
523 keyclass_t keyclass = 0;
524 XCTAssertNoThrow(ks_decrypt_data(KEYBAG_DEVICE, kAKSKeyOpDecrypt, &ac, NULL, enc, NULL, NULL, &attributes, &version, true, &keyclass, &error), @"unexpected exception when decryption fails");
525 XCTAssertEqual(keyclass, key_class_ak, @"failed to get back the keyclass when decryption failed");
527 self.allowDecryption = YES;
531 // these tests fail until we address <rdar://problem/37523001> Fix keychain lock state check to be both secure and fast for EDU mode
532 - (void)testOperationsDontUseCachedKeysWhileLockedWithAKSAvailable // simulating the backup situation
534 self.lockState = LockStateLockedAndAllowAKS;
536 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
537 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
538 (id)kSecAttrAccount : @"TestAccount",
539 (id)kSecAttrService : @"TestService",
540 (id)kSecAttrNoLegacy : @(YES) };
542 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
543 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
545 NSMutableDictionary* metadataQuery = item.mutableCopy;
546 [metadataQuery removeObjectForKey:(id)kSecValueData];
547 metadataQuery[(id)kSecReturnAttributes] = @(YES);
548 CFTypeRef foundItem = NULL;
549 result = SecItemCopyMatching((__bridge CFDictionaryRef)metadataQuery, &foundItem);
550 XCTAssertEqual(result, 0, @"failed to find the metadata for the item we just added in the keychain");
552 XCTAssertTrue(self.didAKSDecrypt, @"we did not go through AKS to decrypt the metadata key while locked - bad!");
554 NSMutableDictionary* dataQuery = item.mutableCopy;
555 dataQuery[(id)kSecReturnData] = @(YES);
556 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
557 XCTAssertEqual(result, 0, @"failed to find the data for the item we just added to the keychain");
559 NSData* foundData = (__bridge NSData*)foundItem;
560 if ([foundData isKindOfClass:[NSData class]]) {
561 NSString* foundPassword = [[NSString alloc] initWithData:(__bridge NSData*)foundItem encoding:NSUTF8StringEncoding];
562 XCTAssertEqualObjects(foundPassword, @"password", @"found password (%@) does not match the expected password", foundPassword);
565 XCTAssertTrue(false, @"found data is not the expected class: %@", foundData);
569 - (void)testNoResultsWhenLocked
571 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
572 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
573 (id)kSecAttrAccount : @"TestAccount",
574 (id)kSecAttrService : @"TestService",
575 (id)kSecAttrNoLegacy : @(YES) };
577 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
578 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
580 self.lockState = LockStateLockedAndDisallowAKS;
582 NSMutableDictionary* metadataQuery = item.mutableCopy;
583 [metadataQuery removeObjectForKey:(id)kSecValueData];
584 metadataQuery[(id)kSecReturnAttributes] = @(YES);
585 CFTypeRef foundItem = NULL;
586 result = SecItemCopyMatching((__bridge CFDictionaryRef)metadataQuery, &foundItem);
587 XCTAssertEqual(foundItem, NULL, @"somehow still got results when AKS was locked");
591 - (void)testRecoverFromBadMetadataKey
593 // Disable caching, so we can change AKS encrypt/decrypt
594 id mockSecDbKeychainMetadataKeyStore = OCMClassMock([SecDbKeychainMetadataKeyStore class]);
595 OCMStub([mockSecDbKeychainMetadataKeyStore cachingEnabled]).andReturn(false);
597 NSDictionary* addQuery = @{ (id)kSecClass : (id)kSecClassGenericPassword,
598 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
599 (id)kSecAttrAccount : @"TestAccount",
600 (id)kSecAttrService : @"TestService",
601 (id)kSecAttrNoLegacy : @(YES),
602 (id)kSecReturnAttributes : @(YES),
605 NSDictionary* findQuery = @{ (id)kSecClass : (id)kSecClassGenericPassword,
606 (id)kSecAttrAccount : @"TestAccount",
607 (id)kSecAttrService : @"TestService",
608 (id)kSecAttrNoLegacy : @(YES),
609 (id)kSecReturnAttributes : @(YES),
613 NSDictionary* updateQuery = findQuery;
615 // iOS won't tolerate kSecReturnAttributes in SecItemUpdate
616 NSDictionary* updateQuery = @{ (id)kSecClass : (id)kSecClassGenericPassword,
617 (id)kSecAttrAccount : @"TestAccount",
618 (id)kSecAttrService : @"TestService",
619 (id)kSecAttrNoLegacy : @(YES),
623 NSDictionary* addQuery2 = @{ (id)kSecClass : (id)kSecClassGenericPassword,
624 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
625 (id)kSecAttrAccount : @"TestAccount-second",
626 (id)kSecAttrService : @"TestService-second",
627 (id)kSecAttrNoLegacy : @(YES),
628 (id)kSecReturnAttributes : @(YES),
631 NSDictionary* findQuery2 = @{ (id)kSecClass : (id)kSecClassGenericPassword,
632 (id)kSecAttrAccount : @"TestAccount-second",
633 (id)kSecAttrService : @"TestService-second",
634 (id)kSecAttrNoLegacy : @(YES),
635 (id)kSecReturnAttributes : @(YES),
638 CFTypeRef result = NULL;
641 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)addQuery, &result), errSecSuccess, @"Should have succeeded in adding test item to keychain");
642 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd");
643 CFReleaseNull(result);
645 // Add a second item, for fun and profit
646 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)addQuery2, &result),
648 @"Should have succeeded in adding test2 item to keychain");
650 // And we can find te item
651 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), errSecSuccess, @"Should be able to find item");
652 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemCopyMatching");
653 CFReleaseNull(result);
655 // And we can update the item
656 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQuery,
657 (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"otherpassword" dataUsingEncoding:NSUTF8StringEncoding]}),
659 "Should be able to update an item");
662 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), errSecSuccess, @"Should be able to find item");
663 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemCopyMatching");
664 CFReleaseNull(result);
666 // And we can find the second item
667 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery2, &result),
668 errSecSuccess, @"Should be able to find second item");
669 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemCopyMatching for item 2");
670 CFReleaseNull(result);
672 ///////////////////////////////////////////////////////////////////////////////////
673 // Now, the metadata keys go corrupt (fake that by changing the underlying AKS key)
674 [self setNewFakeAKSKey:[NSData dataWithBytes:"1234567890123456789000" length:32]];
676 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), errSecItemNotFound,
677 "should have received errSecItemNotFound when metadata keys are invalid");
678 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), errSecItemNotFound,
679 "Multiple finds of the same item should receive errSecItemNotFound when metadata keys are invalid");
680 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), errSecItemNotFound,
681 "Multiple finds of the same item should receive errSecItemNotFound when metadata keys are invalid");
683 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery2, &result),
684 errSecItemNotFound, @"Should not be able to find corrupt second item");
685 XCTAssertNil((__bridge id)result, @"Should have received no data back from SICM for corrupt item");
687 // Updating the now-corrupt item should fail
688 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQuery,
689 (__bridge CFDictionaryRef)@{ (id)kSecValueData: [@"otherpassword" dataUsingEncoding:NSUTF8StringEncoding] }),
691 "Should not be able to update a corrupt item");
693 // Re-add the item (should succeed)
694 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)addQuery, &result), errSecSuccess, @"Should have succeeded in adding test item to keychain");
695 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd");
696 CFReleaseNull(result);
698 // And we can find it again
699 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), errSecSuccess, @"Should be able to find item");
700 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd");
701 CFReleaseNull(result);
704 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQuery,
705 (__bridge CFDictionaryRef)@{ (id)kSecValueData: [@"otherpassword" dataUsingEncoding:NSUTF8StringEncoding] }),
707 "Should be able to update a fixed item");
710 // And our second item, which is wrapped under an old key, can't be found
711 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery2, &result),
712 errSecItemNotFound, @"Should not be able to find corrupt second item");
713 XCTAssertNil((__bridge id)result, @"Should have received no data back from SICM for corrupt item");
715 // But can be re-added
716 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)addQuery2, &result),
718 @"Should have succeeded in adding test2 item to keychain after corruption");
719 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd for item 2 (after corruption)");
720 CFReleaseNull(result);
722 // And we can find the second item again
723 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery2, &result),
724 errSecSuccess, @"Should be able to find second item after re-add");
725 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemCopyMatching for item 2 (after re-add)");
726 CFReleaseNull(result);
728 [mockSecDbKeychainMetadataKeyStore stopMocking];
731 // If a metadata key is created during a database transaction which is later rolled back, it shouldn't be cached for use later.
732 - (void)testMetadataKeyDoesntOutliveTxionRollback {
733 NSString* testAccount = @"TestAccount";
734 NSString* otherAccount = @"OtherAccount";
735 NSString* thirdAccount = @"ThirdAccount";
736 [self addTestItemExpecting:errSecSuccess account:testAccount accessible:(id)kSecAttrAccessibleAfterFirstUnlock];
737 [self checkDatabaseExistenceOfMetadataKey:key_class_ck shouldExist:true];
738 [self checkDatabaseExistenceOfMetadataKey:key_class_cku shouldExist:false];
740 // This should fail, and not create a CKU metadata key
741 [self addTestItemExpecting:errSecDuplicateItem account:testAccount accessible:(id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
742 [self checkDatabaseExistenceOfMetadataKey:key_class_ck shouldExist:true];
743 [self checkDatabaseExistenceOfMetadataKey:key_class_cku shouldExist:false];
745 // But successfully creating a new CKU item should create the key
746 [self addTestItemExpecting:errSecSuccess account:otherAccount accessible:(id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
747 [self checkDatabaseExistenceOfMetadataKey:key_class_ck shouldExist:true];
748 [self checkDatabaseExistenceOfMetadataKey:key_class_cku shouldExist:true];
750 // Drop all metadata key caches
751 [SecDbKeychainMetadataKeyStore resetSharedStore];
753 [self findTestItemExpecting:errSecSuccess account:testAccount];
754 [self findTestItemExpecting:errSecSuccess account:otherAccount];
756 // Adding another CKU item now should be fine
757 [self addTestItemExpecting:errSecSuccess account:thirdAccount accessible:(id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
758 [self checkDatabaseExistenceOfMetadataKey:key_class_ck shouldExist:true];
759 [self checkDatabaseExistenceOfMetadataKey:key_class_cku shouldExist:true];
761 // Drop all metadata key caches once more, to ensure we can find all three items from the persisted keys
762 [SecDbKeychainMetadataKeyStore resetSharedStore];
764 [self findTestItemExpecting:errSecSuccess account:testAccount];
765 [self findTestItemExpecting:errSecSuccess account:otherAccount];
766 [self findTestItemExpecting:errSecSuccess account:thirdAccount];
769 - (void)testRecoverDataFromBadKeyclassStorage
771 NSDictionary* metadataAttributesInput = @{@"TestMetadata" : @"TestValue"};
772 SecDbKeychainSerializedItemV7* serializedItem = [self serializedItemWithPassword:@"password" metadataAttributes:metadataAttributesInput];
773 serializedItem.keyclass = (serializedItem.keyclass | key_class_last + 1);
775 NSError* error = nil;
776 SecDbKeychainItemV7* item = [[SecDbKeychainItemV7 alloc] initWithData:serializedItem.data decryptionKeybag:0 error:&error];
777 NSDictionary* metadataAttributesOut = [item metadataAttributesWithError:&error];
778 XCTAssertEqualObjects(metadataAttributesOut, metadataAttributesInput, @"failed to retrieve metadata with error: %@", error);
779 XCTAssertNil(error, @"error encountered attempting to retrieve metadata: %@", error);
782 - (NSData*)performItemEncryptionWithAccessibility:(CFStringRef)accessibility
784 SecAccessControlRef ac = NULL;
785 CFDataRef enc = NULL;
786 CFErrorRef error = NULL;
788 NSDictionary* secretData = @{(id)kSecValueData : @"secret here"};
790 ac = SecAccessControlCreate(NULL, &error);
791 XCTAssertNotNil((__bridge id)ac, @"failed to create access control with error: %@", (__bridge id)error);
792 XCTAssertNil((__bridge id)error, @"encountered error attempting to create access control: %@", (__bridge id)error);
793 XCTAssertTrue(SecAccessControlSetProtection(ac, accessibility, &error), @"failed to set access control protection with error: %@", error);
794 XCTAssertNil((__bridge id)error, @"encountered error attempting to set access control protection: %@", (__bridge id)error);
796 XCTAssertTrue(ks_encrypt_data(KEYBAG_DEVICE, ac, NULL, (__bridge CFDictionaryRef)secretData, (__bridge CFDictionaryRef)@{}, NULL, &enc, true, &error), @"failed to encrypt data with error: %@", error);
797 XCTAssertTrue(enc != NULL, @"failed to get encrypted data from encryption function");
798 XCTAssertNil((__bridge id)error, @"encountered error attempting to encrypt data: %@", (__bridge id)error);
801 return (__bridge_transfer NSData*)enc;
804 - (void)performMetadataDecryptionOfData:(NSData*)encryptedData verifyingAccessibility:(CFStringRef)accessibility
806 CFErrorRef error = NULL;
807 CFMutableDictionaryRef attributes = NULL;
808 uint32_t version = 0;
810 SecAccessControlRef ac = SecAccessControlCreate(NULL, &error);
811 XCTAssertNotNil((__bridge id)ac, @"failed to create access control with error: %@", (__bridge id)error);
812 XCTAssertNil((__bridge id)error, @"encountered error attempting to create access control: %@", (__bridge id)error);
813 XCTAssertTrue(SecAccessControlSetProtection(ac, accessibility, &error), @"failed to set access control protection with error: %@", error);
814 XCTAssertNil((__bridge id)error, @"encountered error attempting to set access control protection: %@", (__bridge id)error);
816 keyclass_t keyclass = 0;
817 XCTAssertTrue(ks_decrypt_data(KEYBAG_DEVICE, kAKSKeyOpDecrypt, &ac, NULL, (__bridge CFDataRef)encryptedData, NULL, NULL, &attributes, &version, false, &keyclass, &error), @"failed to decrypt data with error: %@", error);
818 XCTAssertNil((__bridge id)error, @"encountered error attempting to decrypt data: %@", (__bridge id)error);
819 XCTAssertEqual(keyclass & key_class_last, parse_keyclass(accessibility), @"failed to get back the keyclass from decryption");
821 CFReleaseNull(error);
824 - (void)performMetadataEncryptDecryptWithAccessibility:(CFStringRef)accessibility
826 NSData* encryptedData = [self performItemEncryptionWithAccessibility:accessibility];
828 [SecDbKeychainMetadataKeyStore resetSharedStore];
830 [self performMetadataDecryptionOfData:encryptedData verifyingAccessibility:accessibility];
833 - (void)testMetadataClassKeyDecryptionWithSimulatedAKSRolledKeys
835 self.simulateRolledAKSKey = YES;
837 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleWhenUnlocked];
838 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_ak | key_class_last + 1);
840 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleAfterFirstUnlock];
841 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_ck | key_class_last + 1);
843 #pragma clang diagnostic push
844 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
845 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleAlways];
846 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_dk | key_class_last + 1);
847 #pragma clang diagnostic pop
849 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleWhenUnlockedThisDeviceOnly];
850 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_aku | key_class_last + 1);
852 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
853 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_cku | key_class_last + 1);
855 #pragma clang diagnostic push
856 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
857 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleAlwaysThisDeviceOnly];
858 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_dku | key_class_last + 1);
859 #pragma clang diagnostic pop
861 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly];
862 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_akpu | key_class_last + 1);
865 - (void)testUpgradingMetadataKeyEntry
867 // first, force the creation of a metadata key
868 NSData* encryptedData = [self performItemEncryptionWithAccessibility:kSecAttrAccessibleWhenUnlocked];
870 // now let's jury-rig this metadata key to look like an old one with no actualKeyclass information
871 __block CFErrorRef error = NULL;
872 __block bool ok = true;
873 ok &= kc_with_dbt(true, &error, ^bool(SecDbConnectionRef dbt) {
874 NSString* sql = [NSString stringWithFormat:@"UPDATE metadatakeys SET actualKeyclass = %d WHERE keyclass = %d", 0, key_class_ak];
875 ok &= SecDbPrepare(dbt, (__bridge CFStringRef)sql, &error, ^(sqlite3_stmt* stmt) {
876 ok &= SecDbStep(dbt, stmt, &error, ^(bool* stop) {
884 // now, let's simulate AKS rejecting the decryption, and see if we recover and also update the database
885 self.simulateRolledAKSKey = YES;
886 [SecDbKeychainMetadataKeyStore resetSharedStore];
887 [self performMetadataDecryptionOfData:encryptedData verifyingAccessibility:kSecAttrAccessibleWhenUnlocked];