2 * Copyright (c) 2018 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 "SecKeybagSupport.h"
25 #import "SecDbKeychainItem.h"
26 #import "SecdTestKeychainUtilities.h"
28 #import "SecItemPriv.h"
29 #import "SecItemServer.h"
31 #import <utilities/SecCFWrappers.h>
32 #import <utilities/SecFileLocations.h>
33 #import <SecurityFoundation/SFEncryptionOperation.h>
34 #import <XCTest/XCTest.h>
35 #import <OCMock/OCMock.h>
42 #import "secdmock_db_version_10_5.h"
43 #import "secdmock_db_version_11_1.h"
45 @interface secdmockaks : XCTestCase
46 @property NSString *testHomeDirectory;
47 @property long lockCounter;
50 @implementation secdmockaks
58 * Disable all of SOS syncing since that triggers retains of database
59 * and these tests muck around with the database over and over again, so
60 * that leads to the vnode delete kevent trap triggering for sqlite
61 * over and over again.
64 SecCKKSTestSetDisableSOS(true);
66 //securityd_init(NULL);
69 - (void)createKeychainDirectory
71 [[NSFileManager defaultManager] createDirectoryAtPath:[NSString stringWithFormat: @"%@/Library/Keychains", self.testHomeDirectory] withIntermediateDirectories:YES attributes:nil error:NULL];
74 - (void)removeHomeDirectory
76 if (self.testHomeDirectory) {
77 [[NSFileManager defaultManager] removeItemAtPath:self.testHomeDirectory error:NULL];
84 NSString* testName = [self.name componentsSeparatedByString:@" "][1];
85 testName = [testName stringByReplacingOccurrencesOfString:@"]" withString:@""];
86 secnotice("ckkstest", "Beginning test %@", testName);
88 // Make a new fake keychain
89 self.testHomeDirectory = [NSString stringWithFormat: @"/tmp/%@.%X", testName, arc4random()];
90 [self createKeychainDirectory];
92 SetCustomHomeURLString((__bridge CFStringRef) self.testHomeDirectory);
93 static dispatch_once_t onceToken;
94 dispatch_once(&onceToken, ^{
97 SecKeychainDbReset(NULL);
99 // Actually load the database.
100 kc_with_dbt(true, NULL, ^bool (SecDbConnectionRef dbt) { return false; });
105 SetCustomHomeURLString(NULL);
106 SecKeychainDbReset(^{
107 [self removeHomeDirectory];
108 self.testHomeDirectory = nil;
110 //kc_with_dbt(true, NULL, ^bool (SecDbConnectionRef dbt) { return false; });
114 - (void)testAddDeleteItem
116 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
117 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
118 (id)kSecAttrAccount : @"TestAccount",
119 (id)kSecAttrService : @"TestService",
120 (id)kSecAttrNoLegacy : @(YES) };
122 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
123 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
125 NSMutableDictionary* dataQuery = item.mutableCopy;
126 [dataQuery removeObjectForKey:(id)kSecValueData];
127 dataQuery[(id)kSecReturnData] = @(YES);
128 CFTypeRef foundItem = NULL;
129 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
130 XCTAssertEqual(result, 0, @"failed to find the data for the item we just added in the keychain");
132 result = SecItemDelete((__bridge CFDictionaryRef)dataQuery);
133 XCTAssertEqual(result, 0, @"failed to delete item");
137 - (void)createManyItems
140 for (n = 0; n < 50; n++) {
141 NSDictionary* item = @{
142 (id)kSecClass : (id)kSecClassGenericPassword,
143 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
144 (id)kSecAttrAccount : [NSString stringWithFormat:@"TestAccount-%u", n],
145 (id)kSecAttrService : @"TestService",
146 (id)kSecAttrNoLegacy : @(YES)
148 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
149 XCTAssertEqual(result, errSecSuccess, @"failed to add test item to keychain: %u", n);
153 - (void)findManyItems:(unsigned)searchLimit
156 for (n = 0; n < searchLimit; n++) {
157 NSDictionary* item = @{
158 (id)kSecClass : (id)kSecClassGenericPassword,
159 (id)kSecAttrAccount : [NSString stringWithFormat:@"TestAccount-%u", n],
160 (id)kSecAttrService : @"TestService",
161 (id)kSecAttrNoLegacy : @(YES)
163 OSStatus result = SecItemCopyMatching((__bridge CFDictionaryRef)item, NULL);
164 XCTAssertEqual(result, errSecSuccess, @"failed to find test item to keychain: %u", n);
168 - (void)testSecItemServerDeleteAll
170 // BT root key, should not be deleted
171 NSMutableDictionary* bt = [@{
172 (id)kSecClass : (id)kSecClassGenericPassword,
173 (id)kSecAttrAccessGroup : @"com.apple.bluetooth",
174 (id)kSecAttrService : @"BluetoothGlobal",
175 (id)kSecAttrAccessible : (id)kSecAttrAccessibleAlwaysThisDeviceOnlyPrivate,
176 (id)kSecAttrSynchronizable : @(NO),
177 (id)kSecValueData : [@"btkey" dataUsingEncoding:NSUTF8StringEncoding],
180 // lockdown-identities, should not be deleted
181 NSMutableDictionary* ld = [@{
182 (id)kSecClass : (id)kSecClassKey,
183 (id)kSecAttrAccessGroup : @"lockdown-identities",
184 (id)kSecAttrLabel : @"com.apple.lockdown.identity.activation",
185 (id)kSecAttrAccessible : (id)kSecAttrAccessibleAlwaysThisDeviceOnlyPrivate,
186 (id)kSecAttrSynchronizable : @(NO),
187 (id)kSecValueData : [@"ldkey" dataUsingEncoding:NSUTF8StringEncoding],
190 // general nonsyncable item, should be deleted
191 NSMutableDictionary* s0 = [@{
192 (id)kSecClass : (id)kSecClassGenericPassword,
193 (id)kSecAttrService : @"NonsyncableService",
194 (id)kSecAttrSynchronizable : @(NO),
195 (id)kSecValueData : [@"s0pwd" dataUsingEncoding:NSUTF8StringEncoding],
198 // general syncable item, should be deleted
199 NSMutableDictionary* s1 = [@{
200 (id)kSecClass : (id)kSecClassGenericPassword,
201 (id)kSecAttrService : @"SyncableService",
202 (id)kSecAttrSynchronizable : @(YES),
203 (id)kSecValueData : [@"s0pwd" dataUsingEncoding:NSUTF8StringEncoding],
208 status = SecItemAdd((__bridge CFDictionaryRef)bt, NULL);
209 XCTAssertEqual(status, errSecSuccess, "failed to add bt item to keychain");
210 status = SecItemAdd((__bridge CFDictionaryRef)ld, NULL);
211 XCTAssertEqual(status, errSecSuccess, "failed to add ld item to keychain");
212 status = SecItemAdd((__bridge CFDictionaryRef)s0, NULL);
213 XCTAssertEqual(status, errSecSuccess, "failed to add s0 item to keychain");
214 status = SecItemAdd((__bridge CFDictionaryRef)s1, NULL);
215 XCTAssertEqual(status, errSecSuccess, "failed to add s1 item to keychain");
217 // Make sure they exist now
218 bt[(id)kSecValueData] = nil;
219 ld[(id)kSecValueData] = nil;
220 s0[(id)kSecValueData] = nil;
221 s1[(id)kSecValueData] = nil;
222 status = SecItemCopyMatching((__bridge CFDictionaryRef)bt, NULL);
223 XCTAssertEqual(status, errSecSuccess, "failed to find bt item in keychain");
224 status = SecItemCopyMatching((__bridge CFDictionaryRef)ld, NULL);
225 XCTAssertEqual(status, errSecSuccess, "failed to find ld item in keychain");
226 status = SecItemCopyMatching((__bridge CFDictionaryRef)s0, NULL);
227 XCTAssertEqual(status, errSecSuccess, "failed to find s0 item in keychain");
228 status = SecItemCopyMatching((__bridge CFDictionaryRef)s1, NULL);
229 XCTAssertEqual(status, errSecSuccess, "failed to find s1 item in keychain");
232 CFErrorRef error = NULL;
233 _SecItemDeleteAll(&error);
234 XCTAssertEqual(error, NULL, "_SecItemDeleteAll returned an error: %@", error);
235 CFReleaseNull(error);
237 // Does the function work properly with an error pre-set?
238 error = CFErrorCreate(kCFAllocatorDefault, kCFErrorDomainOSStatus, errSecItemNotFound, NULL);
239 _SecItemDeleteAll(&error);
240 XCTAssertEqual(CFErrorGetDomain(error), kCFErrorDomainOSStatus);
241 XCTAssertEqual(CFErrorGetCode(error), errSecItemNotFound);
242 CFReleaseNull(error);
244 // Check the relevant items are missing
245 status = SecItemCopyMatching((__bridge CFDictionaryRef)bt, NULL);
246 XCTAssertEqual(status, errSecSuccess, "failed to find bt item in keychain");
247 status = SecItemCopyMatching((__bridge CFDictionaryRef)ld, NULL);
248 XCTAssertEqual(status, errSecSuccess, "failed to find ld item in keychain");
249 status = SecItemCopyMatching((__bridge CFDictionaryRef)s0, NULL);
250 XCTAssertEqual(status, errSecItemNotFound, "unexpectedly found s0 item in keychain");
251 status = SecItemCopyMatching((__bridge CFDictionaryRef)s1, NULL);
252 XCTAssertEqual(status, errSecItemNotFound, "unexpectedly found s1 item in keychain");
255 - (void)createManyKeys
258 for (n = 0; n < 50; n++) {
259 NSDictionary* keyParams = @{
260 (id)kSecAttrKeyType : (id)kSecAttrKeyTypeRSA,
261 (id)kSecAttrKeySizeInBits : @(1024)
263 SecKeyRef key = SecKeyCreateRandomKey((__bridge CFDictionaryRef)keyParams, NULL);
264 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassKey,
265 (id)kSecValueRef : (__bridge id)key,
266 (id)kSecAttrLabel : [NSString stringWithFormat:@"TestLabel-%u", n],
267 (id)kSecAttrNoLegacy : @(YES) };
269 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
270 XCTAssertEqual(result, 0, @"failed to add test key to keychain: %u", n);
275 - (void)testBackupRestoreItem
277 [self createManyItems];
278 [self createManyKeys];
281 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassGenericPassword,
282 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
283 (id)kSecAttrAccount : @"TestAccount",
284 (id)kSecAttrService : @"TestService",
285 (id)kSecAttrNoLegacy : @(YES) };
287 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
288 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
290 NSMutableDictionary* dataQuery = item.mutableCopy;
291 [dataQuery removeObjectForKey:(id)kSecValueData];
292 dataQuery[(id)kSecReturnData] = @(YES);
293 CFTypeRef foundItem = NULL;
299 CFDataRef keybag = CFDataCreate(kCFAllocatorDefault, NULL, 0);
300 CFDataRef password = CFDataCreate(kCFAllocatorDefault, NULL, 0);
302 CFDataRef backup = _SecKeychainCopyBackup(keybag, password);
303 XCTAssert(backup, "expected to have a backup");
305 result = SecItemDelete((__bridge CFDictionaryRef)dataQuery);
306 XCTAssertEqual(result, 0, @"failed to delete item");
308 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
309 XCTAssertEqual(result, errSecItemNotFound,
310 @"failed to find the data for the item we just added in the keychain");
311 CFReleaseNull(foundItem);
314 * Restore backup and see that item is resurected
317 XCTAssertEqual(0, _SecKeychainRestoreBackup(backup, keybag, password));
319 CFReleaseNull(backup);
320 CFReleaseNull(password);
321 CFReleaseNull(keybag);
323 result = SecItemCopyMatching((__bridge CFDictionaryRef)dataQuery, &foundItem);
324 XCTAssertEqual(result, 0, @"failed to find the data for the item we just added in the keychain");
325 CFReleaseNull(foundItem);
327 result = SecItemDelete((__bridge CFDictionaryRef)dataQuery);
328 XCTAssertEqual(result, 0, @"failed to delete item");
331 - (void)testCreateSampleDatabase
334 id mock = OCMClassMock([SecMockAKS class]);
335 OCMStub([mock useGenerationCount]).andReturn(true);
338 [self createManyItems];
339 [self createManyKeys];
343 lsof -p $(pgrep xctest)
348 add header and footer
351 [self findManyItems:50];
354 - (void)testTestAKSGenerationCount
357 id mock = OCMClassMock([SecMockAKS class]);
358 OCMStub([mock useGenerationCount]).andReturn(true);
360 [self createManyItems];
361 [self findManyItems:50];
366 - (void)loadDatabase:(const char **)dumpstring
371 [self removeHomeDirectory];
372 [self createKeychainDirectory];
374 NSString *path = CFBridgingRelease(__SecKeychainCopyPath());
375 sqlite3 *handle = NULL;
377 XCTAssertEqual(SQLITE_OK, sqlite3_open([path UTF8String], &handle), "create keychain");
379 while ((s = dumpstring[n++]) != NULL) {
380 char * errmsg = NULL;
381 XCTAssertEqual(SQLITE_OK, sqlite3_exec(handle, s, NULL, NULL, &errmsg),
382 "exec: %s: %s", s, errmsg);
384 sqlite3_free(errmsg);
387 XCTAssertEqual(SQLITE_OK, sqlite3_close(handle), "close sqlite");
390 - (void)checkIncremental
393 * check that we made incremental vacuum mode
396 __block CFErrorRef localError = NULL;
397 __block bool ok = true;
398 __block int vacuumMode = -1;
400 kc_with_dbt(true, NULL, ^bool (SecDbConnectionRef dbt) {
401 ok &= SecDbPrepare(dbt, CFSTR("PRAGMA auto_vacuum"), &localError, ^(sqlite3_stmt *stmt) {
402 ok = SecDbStep(dbt, stmt, NULL, ^(bool *stop) {
403 vacuumMode = sqlite3_column_int(stmt, 0);
408 XCTAssertEqual(ok, true, "should work to fetch auto_vacuum value: %@", localError);
409 XCTAssertEqual(vacuumMode, 2, "vacuum mode should be incremental (2)");
411 CFReleaseNull(localError);
415 - (void)testUpgradeFromVersion10_5
417 SecKeychainDbReset(^{
418 NSLog(@"resetting database");
419 [self loadDatabase:secdmock_db_version10_5];
422 NSLog(@"find items from old database");
423 [self findManyItems:50];
425 [self checkIncremental];
428 - (void)testUpgradeFromVersion11_1
430 SecKeychainDbReset(^{
431 NSLog(@"resetting database");
432 [self loadDatabase:secdmock_db_version11_1];
435 NSLog(@"find items from old database");
436 [self findManyItems:50];
438 [self checkIncremental];
443 - (void)testAddKeyByReference
445 NSDictionary* keyParams = @{ (id)kSecAttrKeyType : (id)kSecAttrKeyTypeRSA, (id)kSecAttrKeySizeInBits : @(1024) };
446 SecKeyRef key = SecKeyCreateRandomKey((__bridge CFDictionaryRef)keyParams, NULL);
447 NSDictionary* item = @{ (id)kSecClass : (id)kSecClassKey,
448 (id)kSecValueRef : (__bridge id)key,
449 (id)kSecAttrLabel : @"TestLabel",
450 (id)kSecAttrNoLegacy : @(YES) };
452 OSStatus result = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
453 XCTAssertEqual(result, 0, @"failed to add test item to keychain");
455 NSMutableDictionary* refQuery = item.mutableCopy;
456 [refQuery removeObjectForKey:(id)kSecValueData];
457 refQuery[(id)kSecReturnRef] = @(YES);
458 CFTypeRef foundItem = NULL;
459 result = SecItemCopyMatching((__bridge CFDictionaryRef)refQuery, &foundItem);
460 XCTAssertEqual(result, 0, @"failed to find the reference for the item we just added in the keychain");
462 NSData* originalKeyData = (__bridge_transfer NSData*)SecKeyCopyExternalRepresentation(key, NULL);
463 NSData* foundKeyData = (__bridge_transfer NSData*)SecKeyCopyExternalRepresentation((SecKeyRef)foundItem, NULL);
464 XCTAssertEqualObjects(originalKeyData, foundKeyData, @"found key does not match the key we put in the keychain");
466 result = SecItemDelete((__bridge CFDictionaryRef)refQuery);
467 XCTAssertEqual(result, 0, @"failed to delete key");
470 - (bool)isLockedSoon:(keyclass_t)key_class
472 if (key_class == key_class_d || key_class == key_class_dku)
474 if (self.lockCounter <= 0)
481 * Lock in the middle of migration
483 - (void)testUpgradeFromVersion10_5WhileLocked
486 id mock = OCMClassMock([SecMockAKS class]);
487 [[[[mock stub] andCall:@selector(isLockedSoon:) onObject:self] ignoringNonObjectArgs] isLocked:0];
489 SecKeychainDbReset(^{
490 NSLog(@"resetting database");
491 [self loadDatabase:secdmock_db_version10_5];
494 self.lockCounter = 0;
496 NSDictionary* item = @{
497 (id)kSecClass : (id)kSecClassGenericPassword,
498 (id)kSecAttrAccount : @"TestAccount-11",
499 (id)kSecAttrService : @"TestService",
500 (id)kSecReturnData : @YES,
501 (id)kSecAttrNoLegacy : @YES
503 result = SecItemCopyMatching((__bridge CFDictionaryRef)item, NULL);
504 XCTAssertEqual(result, errSecInteractionNotAllowed, @"SEP not locked?");
506 XCTAssertEqual(self.lockCounter, 0, "Device didn't lock");
508 NSLog(@"user unlock");
512 result = SecItemCopyMatching((__bridge CFDictionaryRef)item, NULL);
513 XCTAssertEqual(result, 0, @"can't find item");
516 NSLog(@"find items from old database");
517 [self findManyItems:50];
521 - (void)testUpgradeFromVersion10_5HungSEP
523 id mock = OCMClassMock([SecMockAKS class]);
526 OCMStub([mock isSEPDown]).andReturn(true);
528 SecKeychainDbReset(^{
529 NSLog(@"resetting database");
530 [self loadDatabase:secdmock_db_version10_5];
533 NSDictionary* item = @{
534 (id)kSecClass : (id)kSecClassGenericPassword,
535 (id)kSecAttrAccount : @"TestAccount-0",
536 (id)kSecAttrService : @"TestService",
537 (id)kSecAttrNoLegacy : @(YES)
539 result = SecItemCopyMatching((__bridge CFDictionaryRef)item, NULL);
540 XCTAssertEqual(result, errSecNotAvailable, @"SEP not down?");
542 kc_with_dbt(true, NULL, ^bool (SecDbConnectionRef dbt) {
543 CFErrorRef error = NULL;
545 SecKeychainDbGetVersion(dbt, &version, &error);
546 XCTAssertEqual(error, NULL, "error getting version");
547 XCTAssertEqual(version, 0x50a, "managed to upgrade when we shouldn't have");
552 /* user got the SEP out of DFU */
556 result = SecItemCopyMatching((__bridge CFDictionaryRef)item, NULL);
557 XCTAssertEqual(result, 0, @"failed to find test item to keychain");
559 kc_with_dbt(true, NULL, ^bool (SecDbConnectionRef dbt) {
560 CFErrorRef error = NULL;
562 SecKeychainDbGetVersion(dbt, &version, &error);
563 XCTAssertEqual(error, NULL, "error getting version");
564 XCTAssertEqual(version, 0x40b, "didnt managed to upgrade");
569 NSLog(@"find items from old database");
570 [self findManyItems:50];
573 #endif /* USE_KEYSTORE */