]> git.saurik.com Git - apple/security.git/blob - secdxctests/KeychainCryptoTests.m
Security-58286.51.6.tar.gz
[apple/security.git] / secdxctests / KeychainCryptoTests.m
1 /*
2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
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
11 * file.
12 *
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.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #import "KeychainXCTest.h"
25 #import "SecDbKeychainItem.h"
26 #import "SecdTestKeychainUtilities.h"
27 #import "CKKS.h"
28 #import "SecDbKeychainItemV7.h"
29 #import "SecItemPriv.h"
30 #import "SecItemServer.h"
31 #import "spi.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>
41 #import <notify.h>
42
43 @interface SecDbKeychainItemV7 ()
44
45 + (SFAESKeySpecifier*)keySpecifier;
46
47 @end
48
49 #if USE_KEYSTORE
50 #include <libaks.h>
51 #endif
52
53 @interface KeychainCryptoTests : KeychainXCTest
54 @end
55
56 @implementation KeychainCryptoTests
57
58 #if USE_KEYSTORE
59 #include <libaks.h>
60
61 static keyclass_t parse_keyclass(CFTypeRef value) {
62 if (!value || CFGetTypeID(value) != CFStringGetTypeID()) {
63 return 0;
64 }
65
66 if (CFEqual(value, kSecAttrAccessibleWhenUnlocked)) {
67 return key_class_ak;
68 }
69 else if (CFEqual(value, kSecAttrAccessibleAfterFirstUnlock)) {
70 return key_class_ck;
71 }
72 else if (CFEqual(value, kSecAttrAccessibleAlwaysPrivate)) {
73 return key_class_dk;
74 }
75 else if (CFEqual(value, kSecAttrAccessibleWhenUnlockedThisDeviceOnly)) {
76 return key_class_aku;
77 }
78 else if (CFEqual(value, kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)) {
79 return key_class_cku;
80 }
81 else if (CFEqual(value, kSecAttrAccessibleAlwaysThisDeviceOnlyPrivate)) {
82 return key_class_dku;
83 }
84 else if (CFEqual(value, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly)) {
85 return key_class_akpu;
86 }
87 else {
88 return 0;
89 }
90 }
91
92 - (void)testBasicEncryptDecrypt
93 {
94 CFDataRef enc = NULL;
95 CFErrorRef error = NULL;
96 SecAccessControlRef ac = NULL;
97
98 NSDictionary* secretData = @{(id)kSecValueData : @"secret here"};
99
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);
105
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);
109 CFReleaseNull(ac);
110
111 CFMutableDictionaryRef attributes = NULL;
112 uint32_t version = 0;
113
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");
118
119 CFTypeRef aclProtection = ac ? SecAccessControlGetProtection(ac) : NULL;
120 XCTAssertNotNil((__bridge id)aclProtection, @"failed to get ACL from keychain item decryption");
121 if (aclProtection) {
122 XCTAssertTrue(CFEqual(aclProtection, kSecAttrAccessibleWhenUnlocked), @"the acl we got back from decryption does not match what we put in");
123 }
124 CFReleaseNull(ac);
125
126 CFReleaseNull(error);
127 CFReleaseNull(enc);
128 }
129
130 - (void)testGetMetadataThenData
131 {
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) };
137
138 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
139 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
140
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");
147
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");
154
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);
159 }
160 else {
161 XCTAssertTrue(false, @"found data is not the expected class: %@", foundData);
162 }
163 }
164
165 - (void)testGetReference
166 {
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) };
173
174 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
175 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
176
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");
183
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");
187 }
188
189 - (void)testMetadataQueriesDoNotGetSecret
190 {
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) };
196
197 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
198 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
199
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");
206
207 NSData* data = [(__bridge NSDictionary*)foundItem valueForKey:(id)kSecValueData];
208 XCTAssertNil(data, @"unexpectedly found data in a metadata query");
209 }
210
211 - (void)testDeleteItem
212 {
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) };
218
219 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
220 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
221
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");
228
229 result = SecItemDelete((__bridge CFDictionaryRef)dataQuery);
230 XCTAssertEqual(result, 0, @"failed to delete item");
231 }
232
233 - (SecDbKeychainSerializedItemV7*)serializedItemWithPassword:(NSString*)password metadataAttributes:(NSDictionary*)metadata
234 {
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;
243 }
244
245 - (void)testTamperChecksThwartTampering
246 {
247 SecDbKeychainSerializedItemV7* serializedItem1 = [self serializedItemWithPassword:@"first password" metadataAttributes:nil];
248 SecDbKeychainSerializedItemV7* serializedItem2 = [self serializedItemWithPassword:@"second password" metadataAttributes:nil];
249
250 serializedItem1.encryptedSecretData = serializedItem2.encryptedSecretData;
251 NSData* tamperedSerializedItemBlob = serializedItem1.data;
252
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");
257 }
258
259 - (void)testCacheExpiration
260 {
261
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 };
268
269 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
270 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
271
272 NSMutableDictionary* dataQuery = item.mutableCopy;
273 [dataQuery removeObjectForKey:(id)kSecValueData];
274 dataQuery[(id)kSecReturnData] = @(YES);
275
276 CFTypeRef foundItem = NULL;
277
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);
281
282 self.lockState = LockStateLockedAndDisallowAKS;
283
284 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
285 XCTAssertEqual(result, errSecInteractionNotAllowed, @"get the lock error");
286 XCTAssertEqual(foundItem, NULL, @"got item anyway: %@", foundItem);
287
288 self.lockState = LockStateUnlocked;
289
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);
293
294 result = SecItemDelete((__bridge CFDictionaryRef)dataQuery);
295 XCTAssertEqual(result, 0, @"failed to delete item");
296 }
297
298 - (void)trashMetadataClassAKey
299 {
300 CFErrorRef cferror = NULL;
301
302 kc_with_dbt(true, &cferror, ^bool(SecDbConnectionRef dbt) {
303 CFErrorRef errref;
304 SecDbExec(dbt, CFSTR("DELETE FROM metadatakeys WHERE keyclass = '6'"), &errref);
305 CFReleaseNull(errref);
306 return true;
307 });
308 CFReleaseNull(cferror);
309
310 [[SecDbKeychainMetadataKeyStore sharedStore] dropClassAKeys];
311 }
312
313 - (void)testKeychainCorruptionCopyMatching
314 {
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 };
321
322 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
323 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
324
325
326 NSMutableDictionary* dataQuery = item.mutableCopy;
327 [dataQuery removeObjectForKey:(id)kSecValueData];
328 dataQuery[(id)kSecReturnData] = @(YES);
329
330 CFTypeRef foundItem = NULL;
331
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);
335
336 [self trashMetadataClassAKey];
337
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);
342
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");
346 }
347
348 - (void)testKeychainCorruptionAddOverCorruptedEntry
349 {
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 };
357
358 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
359 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
360
361 NSMutableDictionary* dataQuery = item.mutableCopy;
362 [dataQuery removeObjectForKey:(id)kSecValueData];
363 dataQuery[(id)kSecReturnData] = @(YES);
364
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);
368
369 [self trashMetadataClassAKey];
370
371 result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
372 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
373
374 result = SecItemDelete((__bridge CFDictionaryRef)dataQuery);
375 XCTAssertEqual(result, 0, @"failed to delete item");
376 }
377
378 - (void)testKeychainCorruptionUpdateCorruptedEntry
379 {
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 };
387
388 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
389 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
390
391 NSMutableDictionary* dataQuery = item.mutableCopy;
392 [dataQuery removeObjectForKey:(id)kSecValueData];
393 dataQuery[(id)kSecReturnData] = @(YES);
394
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);
398
399 [self trashMetadataClassAKey];
400
401 NSMutableDictionary* updateQuery = item.mutableCopy;
402 updateQuery[(id)kSecValueData] = NULL;
403 NSDictionary *updateData = @{
404 (id)kSecValueData : [@"foo" dataUsingEncoding:NSUTF8StringEncoding],
405 };
406
407 result = SecItemUpdate((__bridge CFDictionaryRef)updateQuery,
408 (__bridge CFDictionaryRef)updateData );
409 XCTAssertEqual(result, errSecItemNotFound, @"failed to add test item to keychain");
410
411 result = SecItemDelete((__bridge CFDictionaryRef)dataQuery);
412 XCTAssertEqual(result, 0, @"failed to delete item");
413 }
414
415 - (id)encryptionOperation
416 {
417 return nil;
418 }
419
420 - (void)testNoCrashWhenMetadataDecryptionFails
421 {
422 CFDataRef enc = NULL;
423 CFErrorRef error = NULL;
424 SecAccessControlRef ac = NULL;
425
426 self.allowDecryption = NO;
427
428 NSDictionary* secretData = @{(id)kSecValueData : @"secret here"};
429
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);
435
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);
439 CFReleaseNull(ac);
440
441 CFMutableDictionaryRef attributes = NULL;
442 uint32_t version = 0;
443
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");
447
448 self.allowDecryption = YES;
449 }
450
451 #if 0
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
454 {
455 self.lockState = LockStateLockedAndAllowAKS;
456
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) };
462
463 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
464 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
465
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");
472
473 XCTAssertTrue(self.didAKSDecrypt, @"we did not go through AKS to decrypt the metadata key while locked - bad!");
474
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");
479
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);
484 }
485 else {
486 XCTAssertTrue(false, @"found data is not the expected class: %@", foundData);
487 }
488 }
489
490 - (void)testNoResultsWhenLocked
491 {
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) };
497
498 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
499 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
500
501 self.lockState = LockStateLockedAndDisallowAKS;
502
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");
509 }
510 #endif
511
512 - (void)testRecoverFromBadMetadataKey
513 {
514 // Disable caching, so we can change AKS encrypt/decrypt
515 id mockSecDbKeychainMetadataKeyStore = OCMClassMock([SecDbKeychainMetadataKeyStore class]);
516 OCMStub([mockSecDbKeychainMetadataKeyStore cachingEnabled]).andReturn(false);
517
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),
524 };
525
526 NSDictionary* findQuery = @{ (id)kSecClass : (id)kSecClassGenericPassword,
527 (id)kSecAttrAccount : @"TestAccount",
528 (id)kSecAttrService : @"TestService",
529 (id)kSecAttrNoLegacy : @(YES),
530 (id)kSecReturnAttributes : @(YES),
531 };
532
533 #if TARGET_OS_OSX
534 NSDictionary* updateQuery = findQuery;
535 #else
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),
541 };
542 #endif
543
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),
550 };
551
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),
557 };
558
559 CFTypeRef result = NULL;
560
561 // Add the item
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);
565
566 // Add a second item, for fun and profit
567 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)addQuery2, &result),
568 errSecSuccess,
569 @"Should have succeeded in adding test2 item to keychain");
570
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);
575
576 // And we can update the item
577 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQuery,
578 (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"otherpassword" dataUsingEncoding:NSUTF8StringEncoding]}),
579 errSecSuccess,
580 "Should be able to update an item");
581
582 // And find it again
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);
586
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);
592
593 ///////////////////////////////////////////////////////////////////////////////////
594 // Now, the metadata keys go corrupt (fake that by changing the underlying AKS key)
595 [self setNewFakeAKSKey:[NSData dataWithBytes:"1234567890123456789000" length:32]];
596
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");
603
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");
607
608 // Updating the now-corrupt item should fail
609 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQuery,
610 (__bridge CFDictionaryRef)@{ (id)kSecValueData: [@"otherpassword" dataUsingEncoding:NSUTF8StringEncoding] }),
611 errSecItemNotFound,
612 "Should not be able to update a corrupt item");
613
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);
618
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);
623
624 // And update it
625 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQuery,
626 (__bridge CFDictionaryRef)@{ (id)kSecValueData: [@"otherpassword" dataUsingEncoding:NSUTF8StringEncoding] }),
627 errSecSuccess,
628 "Should be able to update a fixed item");
629
630 /////////////
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");
635
636 // But can be re-added
637 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)addQuery2, &result),
638 errSecSuccess,
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);
642
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);
648
649 [mockSecDbKeychainMetadataKeyStore stopMocking];
650 }
651
652 - (void)testRecoverDataFromBadKeyclassStorage
653 {
654 NSDictionary* metadataAttributesInput = @{@"TestMetadata" : @"TestValue"};
655 SecDbKeychainSerializedItemV7* serializedItem = [self serializedItemWithPassword:@"password" metadataAttributes:metadataAttributesInput];
656 serializedItem.keyclass = (serializedItem.keyclass | key_class_last + 1);
657
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);
663 }
664
665 - (NSData*)performItemEncryptionWithAccessibility:(CFStringRef)accessibility
666 {
667 SecAccessControlRef ac = NULL;
668 CFDataRef enc = NULL;
669 CFErrorRef error = NULL;
670
671 NSDictionary* secretData = @{(id)kSecValueData : @"secret here"};
672
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);
678
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);
682 CFReleaseNull(ac);
683
684 return (__bridge_transfer NSData*)enc;
685 }
686
687 - (void)performMetadataDecryptionOfData:(NSData*)encryptedData verifyingAccessibility:(CFStringRef)accessibility
688 {
689 CFErrorRef error = NULL;
690 CFMutableDictionaryRef attributes = NULL;
691 uint32_t version = 0;
692
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);
698
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");
703
704 CFReleaseNull(error);
705 }
706
707 - (void)performMetadataEncryptDecryptWithAccessibility:(CFStringRef)accessibility
708 {
709 NSData* encryptedData = [self performItemEncryptionWithAccessibility:accessibility];
710
711 [SecDbKeychainMetadataKeyStore resetSharedStore];
712
713 [self performMetadataDecryptionOfData:encryptedData verifyingAccessibility:accessibility];
714 }
715
716 - (void)testMetadataClassKeyDecryptionWithSimulatedAKSRolledKeys
717 {
718 self.simulateRolledAKSKey = YES;
719
720 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleWhenUnlocked];
721 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_ak | key_class_last + 1);
722
723 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleAfterFirstUnlock];
724 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_ck | key_class_last + 1);
725
726 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleAlways];
727 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_dk | key_class_last + 1);
728
729 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleWhenUnlockedThisDeviceOnly];
730 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_aku | key_class_last + 1);
731
732 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
733 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_cku | key_class_last + 1);
734
735 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleAlwaysThisDeviceOnly];
736 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_dku | key_class_last + 1);
737
738 [self performMetadataEncryptDecryptWithAccessibility:kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly];
739 XCTAssertEqual(self.keyclassUsedForAKSDecryption, key_class_akpu | key_class_last + 1);
740 }
741
742 - (void)testUpgradingMetadataKeyEntry
743 {
744 // first, force the creation of a metadata key
745 NSData* encryptedData = [self performItemEncryptionWithAccessibility:kSecAttrAccessibleWhenUnlocked];
746
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) {
754 // woohoo
755 });
756 });
757
758 return ok;
759 });
760
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];
765 }
766
767 #endif
768
769 @end