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;
53 @interface KeychainCryptoTests : KeychainXCTest
56 @implementation KeychainCryptoTests
61 static keyclass_t parse_keyclass(CFTypeRef value) {
62 if (!value || CFGetTypeID(value) != CFStringGetTypeID()) {
66 if (CFEqual(value, kSecAttrAccessibleWhenUnlocked)) {
69 else if (CFEqual(value, kSecAttrAccessibleAfterFirstUnlock)) {
72 else if (CFEqual(value, kSecAttrAccessibleAlwaysPrivate)) {
75 else if (CFEqual(value, kSecAttrAccessibleWhenUnlockedThisDeviceOnly)) {
78 else if (CFEqual(value, kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)) {
81 else if (CFEqual(value, kSecAttrAccessibleAlwaysThisDeviceOnlyPrivate)) {
84 else if (CFEqual(value, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly)) {
85 return key_class_akpu;
92 - (void)testBasicEncryptDecrypt
95 CFErrorRef error = NULL;
96 SecAccessControlRef ac = NULL;
98 NSDictionary* secretData = @{(id)kSecValueData : @"secret here"};
100 ac = SecAccessControlCreate(NULL, &error);
101 XCTAssertNotNil((__bridge id)ac, @"failed to create access control with error: %@", (__bridge id)error);
102 XCTAssertNil((__bridge id)error, @"encountered error attempting to create access control: %@", (__bridge id)error);
103 XCTAssertTrue(SecAccessControlSetProtection(ac, kSecAttrAccessibleWhenUnlocked, &error), @"failed to set access control protection with error: %@", error);
104 XCTAssertNil((__bridge id)error, @"encountered error attempting to set access control protection: %@", (__bridge id)error);
106 XCTAssertTrue(ks_encrypt_data(KEYBAG_DEVICE, ac, NULL, (__bridge CFDictionaryRef)secretData, (__bridge CFDictionaryRef)@{}, NULL, &enc, true, &error), @"failed to encrypt data with error: %@", error);
107 XCTAssertTrue(enc != NULL, @"failed to get encrypted data from encryption function");
108 XCTAssertNil((__bridge id)error, @"encountered error attempting to encrypt data: %@", (__bridge id)error);
111 CFMutableDictionaryRef attributes = NULL;
112 uint32_t version = 0;
114 keyclass_t keyclass = 0;
115 XCTAssertTrue(ks_decrypt_data(KEYBAG_DEVICE, kAKSKeyOpDecrypt, &ac, NULL, enc, NULL, NULL, &attributes, &version, true, &keyclass, &error), @"failed to decrypt data with error: %@", error);
116 XCTAssertNil((__bridge id)error, @"encountered error attempting to decrypt data: %@", (__bridge id)error);
117 XCTAssertEqual(keyclass, key_class_ak, @"failed to get back the keyclass from decryption");
119 CFTypeRef aclProtection = ac ? SecAccessControlGetProtection(ac) : NULL;
120 XCTAssertNotNil((__bridge id)aclProtection, @"failed to get ACL from keychain item decryption");
122 XCTAssertTrue(CFEqual(aclProtection, kSecAttrAccessibleWhenUnlocked), @"the acl we got back from decryption does not match what we put in");
126 CFReleaseNull(error);
130 - (void)testGetMetadataThenData
132 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
133 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
134 (id)kSecAttrAccount : @"TestAccount",
135 (id)kSecAttrService : @"TestService",
136 (id)kSecAttrNoLegacy : @(YES) };
138 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
139 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
141 NSMutableDictionary* metadataQuery = item.mutableCopy;
142 [metadataQuery removeObjectForKey:(id)kSecValueData];
143 metadataQuery[(id)kSecReturnAttributes] = @(YES);
144 CFTypeRef foundItem = NULL;
145 result = SecItemCopyMatching((__bridge CFDictionaryRef)metadataQuery, &foundItem);
146 XCTAssertEqual(result, 0, @"failed to find the metadata for the item we just added in the keychain");
148 NSMutableDictionary* dataQuery = [(__bridge NSDictionary*)foundItem mutableCopy];
149 dataQuery[(id)kSecReturnData] = @(YES);
150 dataQuery[(id)kSecClass] = (id)kSecClassGenericPassword;
151 dataQuery[(id)kSecAttrNoLegacy] = @(YES);
152 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
153 XCTAssertEqual(result, 0, @"failed to find the data for the item we just added to the keychain");
155 NSData* foundData = (__bridge NSData*)foundItem;
156 if ([foundData isKindOfClass:[NSData class]]) {
157 NSString* foundPassword = [[NSString alloc] initWithData:(__bridge NSData*)foundItem encoding:NSUTF8StringEncoding];
158 XCTAssertEqualObjects(foundPassword, @"password", @"found password (%@) does not match the expected password", foundPassword);
161 XCTAssertTrue(false, @"found data is not the expected class: %@", foundData);
165 - (void)testGetReference
167 NSDictionary* keyParams = @{ (id)kSecAttrKeyType : (id)kSecAttrKeyTypeRSA, (id)kSecAttrKeySizeInBits : @(1024) };
168 SecKeyRef key = SecKeyCreateRandomKey((__bridge CFDictionaryRef)keyParams, NULL);
169 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassKey,
170 (id)kSecValueRef : (__bridge id)key,
171 (id)kSecAttrLabel : @"TestLabel",
172 (id)kSecAttrNoLegacy : @(YES) };
174 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
175 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
177 NSMutableDictionary* refQuery = item.mutableCopy;
178 [refQuery removeObjectForKey:(id)kSecValueData];
179 refQuery[(id)kSecReturnRef] = @(YES);
180 CFTypeRef foundItem = NULL;
181 result = SecItemCopyMatching((__bridge CFDictionaryRef)refQuery, &foundItem);
182 XCTAssertEqual(result, 0, @"failed to find the reference for the item we just added in the keychain");
184 NSData* originalKeyData = (__bridge_transfer NSData*)SecKeyCopyExternalRepresentation(key, NULL);
185 NSData* foundKeyData = (__bridge_transfer NSData*)SecKeyCopyExternalRepresentation((SecKeyRef)foundItem, NULL);
186 XCTAssertEqualObjects(originalKeyData, foundKeyData, @"found key does not match the key we put in the keychain");
189 - (void)testMetadataQueriesDoNotGetSecret
191 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
192 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
193 (id)kSecAttrAccount : @"TestAccount",
194 (id)kSecAttrService : @"TestService",
195 (id)kSecAttrNoLegacy : @(YES) };
197 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
198 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
200 NSMutableDictionary* metadataQuery = item.mutableCopy;
201 [metadataQuery removeObjectForKey:(id)kSecValueData];
202 metadataQuery[(id)kSecReturnAttributes] = @(YES);
203 CFTypeRef foundItem = NULL;
204 result = SecItemCopyMatching((__bridge CFDictionaryRef)metadataQuery, &foundItem);
205 XCTAssertEqual(result, 0, @"failed to find the metadata for the item we just added in the keychain");
207 NSData* data = [(__bridge NSDictionary*)foundItem valueForKey:(id)kSecValueData];
208 XCTAssertNil(data, @"unexpectedly found data in a metadata query");
211 - (void)testDeleteItem
213 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
214 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
215 (id)kSecAttrAccount : @"TestAccount",
216 (id)kSecAttrService : @"TestService",
217 (id)kSecAttrNoLegacy : @(YES) };
219 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
220 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
222 NSMutableDictionary* dataQuery = item.mutableCopy;
223 [dataQuery removeObjectForKey:(id)kSecValueData];
224 dataQuery[(id)kSecReturnData] = @(YES);
225 CFTypeRef foundItem = NULL;
226 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
227 XCTAssertEqual(result, 0, @"failed to find the data for the item we just added in the keychain");
229 result = SecItemDelete((__bridge CFDictionaryRef)dataQuery);
230 XCTAssertEqual(result, 0, @"failed to delete item");
233 - (SecDbKeychainSerializedItemV7*)serializedItemWithPassword:(NSString*)password metadataAttributes:(NSDictionary*)metadata
235 SecDbKeychainItemV7* item = [[SecDbKeychainItemV7 alloc] initWithSecretAttributes:@{(id)kSecValueData : password} metadataAttributes:metadata tamperCheck:[[NSUUID UUID] UUIDString] keyclass:9];
236 [item encryptMetadataWithKeybag:0 error:nil];
237 [item encryptSecretDataWithKeybag:0 accessControl:SecAccessControlCreate(NULL, NULL) acmContext:nil error:nil];
238 SecDbKeychainSerializedItemV7* serializedItem = [[SecDbKeychainSerializedItemV7 alloc] init];
239 serializedItem.encryptedMetadata = item.encryptedMetadataBlob;
240 serializedItem.encryptedSecretData = item.encryptedSecretDataBlob;
241 serializedItem.keyclass = 9;
242 return serializedItem;
245 - (void)testTamperChecksThwartTampering
247 SecDbKeychainSerializedItemV7* serializedItem1 = [self serializedItemWithPassword:@"first password" metadataAttributes:nil];
248 SecDbKeychainSerializedItemV7* serializedItem2 = [self serializedItemWithPassword:@"second password" metadataAttributes:nil];
250 serializedItem1.encryptedSecretData = serializedItem2.encryptedSecretData;
251 NSData* tamperedSerializedItemBlob = serializedItem1.data;
253 NSError* error = nil;
254 SecDbKeychainItemV7* item = [[SecDbKeychainItemV7 alloc] initWithData:tamperedSerializedItemBlob decryptionKeybag:0 error:&error];
255 XCTAssertNil(item, @"unexpectedly deserialized an item blob which has been tampered");
256 XCTAssertNotNil(error, @"failed to get an error when deserializing tampered item blob");
259 - (void)testCacheExpiration
262 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
263 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
264 (id)kSecAttrAccount : @"TestAccount",
265 (id)kSecAttrService : @"TestService",
266 (id)kSecAttrAccessible : (id)kSecAttrAccessibleWhenUnlocked,
267 (id)kSecAttrNoLegacy : @YES };
269 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
270 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
272 NSMutableDictionary* dataQuery = item.mutableCopy;
273 [dataQuery removeObjectForKey:(id)kSecValueData];
274 dataQuery[(id)kSecReturnData] = @(YES);
276 CFTypeRef foundItem = NULL;
278 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
279 XCTAssertEqual(result, 0, @"failed to find the data for the item we just added in the keychain");
280 CFReleaseNull(foundItem);
282 self.lockState = LockStateLockedAndDisallowAKS;
284 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
285 XCTAssertEqual(result, errSecInteractionNotAllowed, @"get the lock error");
286 XCTAssertEqual(foundItem, NULL, @"got item anyway: %@", foundItem);
288 self.lockState = LockStateUnlocked;
290 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
291 XCTAssertEqual(result, 0, @"failed to find the data for the item we just added in the keychain");
292 CFReleaseNull(foundItem);
294 result = SecItemDelete((__bridge CFDictionaryRef)dataQuery);
295 XCTAssertEqual(result, 0, @"failed to delete item");
298 - (void)trashMetadataClassAKey
300 CFErrorRef cferror = NULL;
302 kc_with_dbt(true, &cferror, ^bool(SecDbConnectionRef dbt) {
304 SecDbExec(dbt, CFSTR("DELETE FROM metadatakeys WHERE keyclass = '6'"), &errref);
305 CFReleaseNull(errref);
308 CFReleaseNull(cferror);
310 [[SecDbKeychainMetadataKeyStore sharedStore] dropClassAKeys];
313 - (void)testKeychainCorruptionCopyMatching
315 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
316 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
317 (id)kSecAttrAccount : @"TestAccount",
318 (id)kSecAttrService : @"TestService",
319 (id)kSecAttrAccessible : (id)kSecAttrAccessibleWhenUnlocked,
320 (id)kSecAttrNoLegacy : @YES };
322 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
323 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
326 NSMutableDictionary* dataQuery = item.mutableCopy;
327 [dataQuery removeObjectForKey:(id)kSecValueData];
328 dataQuery[(id)kSecReturnData] = @(YES);
330 CFTypeRef foundItem = NULL;
332 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
333 XCTAssertEqual(result, 0, @"failed to find the data for the item we just added in the keychain");
334 CFReleaseNull(foundItem);
336 [self trashMetadataClassAKey];
338 /* when metadata corrupted, we should not find the item */
339 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
340 XCTAssertEqual(result, errSecItemNotFound, @"failed to find the data for the item we just added in the keychain");
341 CFReleaseNull(foundItem);
343 /* semantics are odd, we should be able to delete it */
344 result = SecItemDelete((__bridge CFDictionaryRef)dataQuery);
345 XCTAssertEqual(result, 0, @"failed to delete item");
348 - (void)testKeychainCorruptionAddOverCorruptedEntry
350 CFTypeRef foundItem = NULL;
351 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
352 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
353 (id)kSecAttrAccount : @"TestAccount",
354 (id)kSecAttrService : @"TestService",
355 (id)kSecAttrAccessible : (id)kSecAttrAccessibleWhenUnlocked,
356 (id)kSecAttrNoLegacy : @YES };
358 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
359 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
361 NSMutableDictionary* dataQuery = item.mutableCopy;
362 [dataQuery removeObjectForKey:(id)kSecValueData];
363 dataQuery[(id)kSecReturnData] = @(YES);
365 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
366 XCTAssertEqual(result, 0, @"failed to find the data for the item we just added in the keychain");
367 CFReleaseNull(foundItem);
369 [self trashMetadataClassAKey];
371 result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
372 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
374 result = SecItemDelete((__bridge CFDictionaryRef)dataQuery);
375 XCTAssertEqual(result, 0, @"failed to delete item");
378 - (void)testKeychainCorruptionUpdateCorruptedEntry
380 CFTypeRef foundItem = NULL;
381 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
382 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
383 (id)kSecAttrAccount : @"TestAccount",
384 (id)kSecAttrService : @"TestService",
385 (id)kSecAttrAccessible : (id)kSecAttrAccessibleWhenUnlocked,
386 (id)kSecAttrNoLegacy : @YES };
388 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
389 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
391 NSMutableDictionary* dataQuery = item.mutableCopy;
392 [dataQuery removeObjectForKey:(id)kSecValueData];
393 dataQuery[(id)kSecReturnData] = @(YES);
395 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
396 XCTAssertEqual(result, 0, @"failed to find the data for the item we just added in the keychain");
397 CFReleaseNull(foundItem);
399 [self trashMetadataClassAKey];
401 NSMutableDictionary* updateQuery = item.mutableCopy;
402 updateQuery[(id)kSecValueData] = NULL;
403 NSDictionary *updateData = @{
404 (id)kSecValueData : [@"foo" dataUsingEncoding:NSUTF8StringEncoding],
407 result = SecItemUpdate((__bridge CFDictionaryRef)updateQuery,
408 (__bridge CFDictionaryRef)updateData );
409 XCTAssertEqual(result, errSecItemNotFound, @"failed to add test item to keychain");
411 result = SecItemDelete((__bridge CFDictionaryRef)dataQuery);
412 XCTAssertEqual(result, 0, @"failed to delete item");
415 - (id)encryptionOperation
420 - (void)testNoCrashWhenMetadataDecryptionFails
422 CFDataRef enc = NULL;
423 CFErrorRef error = NULL;
424 SecAccessControlRef ac = NULL;
426 self.allowDecryption = NO;
428 NSDictionary* secretData = @{(id)kSecValueData : @"secret here"};
430 ac = SecAccessControlCreate(NULL, &error);
431 XCTAssertNotNil((__bridge id)ac, @"failed to create access control with error: %@", (__bridge id)error);
432 XCTAssertNil((__bridge id)error, @"encountered error attempting to create access control: %@", (__bridge id)error);
433 XCTAssertTrue(SecAccessControlSetProtection(ac, kSecAttrAccessibleWhenUnlocked, &error), @"failed to set access control protection with error: %@", error);
434 XCTAssertNil((__bridge id)error, @"encountered error attempting to set access control protection: %@", (__bridge id)error);
436 XCTAssertTrue(ks_encrypt_data(KEYBAG_DEVICE, ac, NULL, (__bridge CFDictionaryRef)secretData, (__bridge CFDictionaryRef)@{}, NULL, &enc, true, &error), @"failed to encrypt data with error: %@", error);
437 XCTAssertTrue(enc != NULL, @"failed to get encrypted data from encryption function");
438 XCTAssertNil((__bridge id)error, @"encountered error attempting to encrypt data: %@", (__bridge id)error);
441 CFMutableDictionaryRef attributes = NULL;
442 uint32_t version = 0;
444 keyclass_t keyclass = 0;
445 XCTAssertNoThrow(ks_decrypt_data(KEYBAG_DEVICE, kAKSKeyOpDecrypt, &ac, NULL, enc, NULL, NULL, &attributes, &version, true, &keyclass, &error), @"unexpected exception when decryption fails");
446 XCTAssertEqual(keyclass, key_class_ak, @"failed to get back the keyclass when decryption failed");
448 self.allowDecryption = YES;
452 // these tests fail until we address <rdar://problem/37523001> Fix keychain lock state check to be both secure and fast for EDU mode
453 - (void)testOperationsDontUseCachedKeysWhileLockedWithAKSAvailable // simulating the backup situation
455 self.lockState = LockStateLockedAndAllowAKS;
457 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
458 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
459 (id)kSecAttrAccount : @"TestAccount",
460 (id)kSecAttrService : @"TestService",
461 (id)kSecAttrNoLegacy : @(YES) };
463 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
464 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
466 NSMutableDictionary* metadataQuery = item.mutableCopy;
467 [metadataQuery removeObjectForKey:(id)kSecValueData];
468 metadataQuery[(id)kSecReturnAttributes] = @(YES);
469 CFTypeRef foundItem = NULL;
470 result = SecItemCopyMatching((__bridge CFDictionaryRef)metadataQuery, &foundItem);
471 XCTAssertEqual(result, 0, @"failed to find the metadata for the item we just added in the keychain");
473 XCTAssertTrue(self.didAKSDecrypt, @"we did not go through AKS to decrypt the metadata key while locked - bad!");
475 NSMutableDictionary* dataQuery = item.mutableCopy;
476 dataQuery[(id)kSecReturnData] = @(YES);
477 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
478 XCTAssertEqual(result, 0, @"failed to find the data for the item we just added to the keychain");
480 NSData* foundData = (__bridge NSData*)foundItem;
481 if ([foundData isKindOfClass:[NSData class]]) {
482 NSString* foundPassword = [[NSString alloc] initWithData:(__bridge NSData*)foundItem encoding:NSUTF8StringEncoding];
483 XCTAssertEqualObjects(foundPassword, @"password", @"found password (%@) does not match the expected password", foundPassword);
486 XCTAssertTrue(false, @"found data is not the expected class: %@", foundData);
490 - (void)testNoResultsWhenLocked
492 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
493 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
494 (id)kSecAttrAccount : @"TestAccount",
495 (id)kSecAttrService : @"TestService",
496 (id)kSecAttrNoLegacy : @(YES) };
498 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
499 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
501 self.lockState = LockStateLockedAndDisallowAKS;
503 NSMutableDictionary* metadataQuery = item.mutableCopy;
504 [metadataQuery removeObjectForKey:(id)kSecValueData];
505 metadataQuery[(id)kSecReturnAttributes] = @(YES);
506 CFTypeRef foundItem = NULL;
507 result = SecItemCopyMatching((__bridge CFDictionaryRef)metadataQuery, &foundItem);
508 XCTAssertEqual(foundItem, NULL, @"somehow still got results when AKS was locked");
512 - (void)testRecoverFromBadMetadataKey
514 // Disable caching, so we can change AKS encrypt/decrypt
515 id mockSecDbKeychainMetadataKeyStore = OCMClassMock([SecDbKeychainMetadataKeyStore class]);
516 OCMStub([mockSecDbKeychainMetadataKeyStore cachingEnabled]).andReturn(false);
518 NSDictionary* addQuery = @{ (id)kSecClass : (id)kSecClassGenericPassword,
519 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
520 (id)kSecAttrAccount : @"TestAccount",
521 (id)kSecAttrService : @"TestService",
522 (id)kSecAttrNoLegacy : @(YES),
523 (id)kSecReturnAttributes : @(YES),
526 NSDictionary* findQuery = @{ (id)kSecClass : (id)kSecClassGenericPassword,
527 (id)kSecAttrAccount : @"TestAccount",
528 (id)kSecAttrService : @"TestService",
529 (id)kSecAttrNoLegacy : @(YES),
530 (id)kSecReturnAttributes : @(YES),
534 NSDictionary* updateQuery = findQuery;
536 // iOS won't tolerate kSecReturnAttributes in SecItemUpdate
537 NSDictionary* updateQuery = @{ (id)kSecClass : (id)kSecClassGenericPassword,
538 (id)kSecAttrAccount : @"TestAccount",
539 (id)kSecAttrService : @"TestService",
540 (id)kSecAttrNoLegacy : @(YES),
544 NSDictionary* addQuery2 = @{ (id)kSecClass : (id)kSecClassGenericPassword,
545 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
546 (id)kSecAttrAccount : @"TestAccount-second",
547 (id)kSecAttrService : @"TestService-second",
548 (id)kSecAttrNoLegacy : @(YES),
549 (id)kSecReturnAttributes : @(YES),
552 NSDictionary* findQuery2 = @{ (id)kSecClass : (id)kSecClassGenericPassword,
553 (id)kSecAttrAccount : @"TestAccount-second",
554 (id)kSecAttrService : @"TestService-second",
555 (id)kSecAttrNoLegacy : @(YES),
556 (id)kSecReturnAttributes : @(YES),
559 CFTypeRef result = NULL;
562 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)addQuery, &result), errSecSuccess, @"Should have succeeded in adding test item to keychain");
563 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd");
564 CFReleaseNull(result);
566 // Add a second item, for fun and profit
567 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)addQuery2, &result),
569 @"Should have succeeded in adding test2 item to keychain");
571 // And we can find te item
572 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), errSecSuccess, @"Should be able to find item");
573 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemCopyMatching");
574 CFReleaseNull(result);
576 // And we can update the item
577 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQuery,
578 (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"otherpassword" dataUsingEncoding:NSUTF8StringEncoding]}),
580 "Should be able to update an item");
583 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), errSecSuccess, @"Should be able to find item");
584 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemCopyMatching");
585 CFReleaseNull(result);
587 // And we can find the second item
588 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery2, &result),
589 errSecSuccess, @"Should be able to find second item");
590 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemCopyMatching for item 2");
591 CFReleaseNull(result);
593 ///////////////////////////////////////////////////////////////////////////////////
594 // Now, the metadata keys go corrupt (fake that by changing the underlying AKS key)
595 [self setNewFakeAKSKey:[NSData dataWithBytes:"1234567890123456789000" length:32]];
597 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), errSecItemNotFound,
598 "should have received errSecItemNotFound when metadata keys are invalid");
599 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), errSecItemNotFound,
600 "Multiple finds of the same item should receive errSecItemNotFound when metadata keys are invalid");
601 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), errSecItemNotFound,
602 "Multiple finds of the same item should receive errSecItemNotFound when metadata keys are invalid");
604 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery2, &result),
605 errSecItemNotFound, @"Should not be able to find corrupt second item");
606 XCTAssertNil((__bridge id)result, @"Should have received no data back from SICM for corrupt item");
608 // Updating the now-corrupt item should fail
609 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQuery,
610 (__bridge CFDictionaryRef)@{ (id)kSecValueData: [@"otherpassword" dataUsingEncoding:NSUTF8StringEncoding] }),
612 "Should not be able to update a corrupt item");
614 // Re-add the item (should succeed)
615 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)addQuery, &result), errSecSuccess, @"Should have succeeded in adding test item to keychain");
616 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd");
617 CFReleaseNull(result);
619 // And we can find it again
620 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), errSecSuccess, @"Should be able to find item");
621 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd");
622 CFReleaseNull(result);
625 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQuery,
626 (__bridge CFDictionaryRef)@{ (id)kSecValueData: [@"otherpassword" dataUsingEncoding:NSUTF8StringEncoding] }),
628 "Should be able to update a fixed item");
631 // And our second item, which is wrapped under an old key, can't be found
632 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery2, &result),
633 errSecItemNotFound, @"Should not be able to find corrupt second item");
634 XCTAssertNil((__bridge id)result, @"Should have received no data back from SICM for corrupt item");
636 // But can be re-added
637 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)addQuery2, &result),
639 @"Should have succeeded in adding test2 item to keychain after corruption");
640 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd for item 2 (after corruption)");
641 CFReleaseNull(result);
643 // And we can find the second item again
644 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery2, &result),
645 errSecSuccess, @"Should be able to find second item after re-add");
646 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemCopyMatching for item 2 (after re-add)");
647 CFReleaseNull(result);
649 [mockSecDbKeychainMetadataKeyStore stopMocking];
652 - (void)testRecoverDataFromBadKeyclassStorage
654 NSDictionary* metadataAttributesInput = @{@"TestMetadata" : @"TestValue"};
655 SecDbKeychainSerializedItemV7* serializedItem = [self serializedItemWithPassword:@"password" metadataAttributes:metadataAttributesInput];
656 serializedItem.keyclass = (serializedItem.keyclass | key_class_last + 1);
658 NSError* error = nil;
659 SecDbKeychainItemV7* item = [[SecDbKeychainItemV7 alloc] initWithData:serializedItem.data decryptionKeybag:0 error:&error];
660 NSDictionary* metadataAttributesOut = [item metadataAttributesWithError:&error];
661 XCTAssertEqualObjects(metadataAttributesOut, metadataAttributesInput, @"failed to retrieve metadata with error: %@", error);
662 XCTAssertNil(error, @"error encountered attempting to retrieve metadata: %@", error);
665 - (NSData*)performItemEncryptionWithAccessibility:(CFStringRef)accessibility
667 SecAccessControlRef ac = NULL;
668 CFDataRef enc = NULL;
669 CFErrorRef error = NULL;
671 NSDictionary* secretData = @{(id)kSecValueData : @"secret here"};
673 ac = SecAccessControlCreate(NULL, &error);
674 XCTAssertNotNil((__bridge id)ac, @"failed to create access control with error: %@", (__bridge id)error);
675 XCTAssertNil((__bridge id)error, @"encountered error attempting to create access control: %@", (__bridge id)error);
676 XCTAssertTrue(SecAccessControlSetProtection(ac, accessibility, &error), @"failed to set access control protection with error: %@", error);
677 XCTAssertNil((__bridge id)error, @"encountered error attempting to set access control protection: %@", (__bridge id)error);
679 XCTAssertTrue(ks_encrypt_data(KEYBAG_DEVICE, ac, NULL, (__bridge CFDictionaryRef)secretData, (__bridge CFDictionaryRef)@{}, NULL, &enc, true, &error), @"failed to encrypt data with error: %@", error);
680 XCTAssertTrue(enc != NULL, @"failed to get encrypted data from encryption function");
681 XCTAssertNil((__bridge id)error, @"encountered error attempting to encrypt data: %@", (__bridge id)error);
684 return (__bridge_transfer NSData*)enc;
687 - (void)performMetadataDecryptionOfData:(NSData*)encryptedData verifyingAccessibility:(CFStringRef)accessibility
689 CFErrorRef error = NULL;
690 CFMutableDictionaryRef attributes = NULL;
691 uint32_t version = 0;
693 SecAccessControlRef ac = SecAccessControlCreate(NULL, &error);
694 XCTAssertNotNil((__bridge id)ac, @"failed to create access control with error: %@", (__bridge id)error);
695 XCTAssertNil((__bridge id)error, @"encountered error attempting to create access control: %@", (__bridge id)error);
696 XCTAssertTrue(SecAccessControlSetProtection(ac, accessibility, &error), @"failed to set access control protection with error: %@", error);
697 XCTAssertNil((__bridge id)error, @"encountered error attempting to set access control protection: %@", (__bridge id)error);
699 keyclass_t keyclass = 0;
700 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);
701 XCTAssertNil((__bridge id)error, @"encountered error attempting to decrypt data: %@", (__bridge id)error);
702 XCTAssertEqual(keyclass & key_class_last, parse_keyclass(accessibility), @"failed to get back the keyclass from decryption");
704 CFReleaseNull(error);
707 - (void)performMetadataEncryptDecryptWithAccessibility:(CFStringRef)accessibility
709 NSData* encryptedData = [self performItemEncryptionWithAccessibility:accessibility];
711 [SecDbKeychainMetadataKeyStore resetSharedStore];
713 [self performMetadataDecryptionOfData:encryptedData verifyingAccessibility:accessibility];
716 - (void)testMetadataClassKeyDecryptionWithSimulatedAKSRolledKeys
718 self.simulateRolledAKSKey = YES;
720 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleWhenUnlocked];
721 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_ak | key_class_last + 1);
723 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleAfterFirstUnlock];
724 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_ck | key_class_last + 1);
726 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleAlways];
727 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_dk | key_class_last + 1);
729 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleWhenUnlockedThisDeviceOnly];
730 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_aku | key_class_last + 1);
732 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
733 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_cku | key_class_last + 1);
735 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleAlwaysThisDeviceOnly];
736 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_dku | key_class_last + 1);
738 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly];
739 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_akpu | key_class_last + 1);
742 - (void)testUpgradingMetadataKeyEntry
744 // first, force the creation of a metadata key
745 NSData* encryptedData = [self performItemEncryptionWithAccessibility:kSecAttrAccessibleWhenUnlocked];
747 // now let's jury-rig this metadata key to look like an old one with no actualKeyclass information
748 __block CFErrorRef error = NULL;
749 __block bool ok = true;
750 ok &= kc_with_dbt(true, &error, ^bool(SecDbConnectionRef dbt) {
751 NSString* sql = [NSString stringWithFormat:@"UPDATE metadatakeys SET actualKeyclass = %d WHERE keyclass = %d", 0, key_class_ak];
752 ok &= SecDbPrepare(dbt, (__bridge CFStringRef)sql, &error, ^(sqlite3_stmt* stmt) {
753 ok &= SecDbStep(dbt, stmt, &error, ^(bool* stop) {
761 // now, let's simulate AKS rejecting the decryption, and see if we recover and also update the database
762 self.simulateRolledAKSKey = YES;
763 [SecDbKeychainMetadataKeyStore resetSharedStore];
764 [self performMetadataDecryptionOfData:encryptedData verifyingAccessibility:kSecAttrAccessibleWhenUnlocked];