/ secdxctests / KeychainAPITests.m
KeychainAPITests.m
1 /* 2 * Copyright (c) 2018 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 #include "SecItemInternal.h" 31 #import "SecItemServer.h" 32 #import "spi.h" 33 #import "SecDbKeychainSerializedItemV7.h" 34 #import "SecDbKeychainSerializedMetadata.h" 35 #import "SecDbKeychainSerializedSecretData.h" 36 #import "SecDbKeychainSerializedAKSWrappedKey.h" 37 #import <utilities/SecCFWrappers.h> 38 #import <SecurityFoundation/SFEncryptionOperation.h> 39 #import <XCTest/XCTest.h> 40 #import <OCMock/OCMock.h> 41 #include <dispatch/dispatch.h> 42 #include <utilities/SecDb.h> 43 #include <sys/stat.h> 44 #include <utilities/SecFileLocations.h> 45 #include "der_plist.h" 46 #import "SecItemRateLimit_tests.h" 47 #include "ipc/server_security_helpers.h" 48 #include <Security/SecEntitlements.h> 49 #include "keychain/securityd/SecItemDb.h" 50 51 #if USE_KEYSTORE 52 53 @interface KeychainAPITests : KeychainXCTest 54 @end 55 56 @implementation KeychainAPITests 57 58 + (void)setUp 59 { 60 [super setUp]; 61 } 62 63 - (NSString*)nameOfTest 64 { 65 return [self.name componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@" ]"]][1]; 66 } 67 68 - (void)setUp 69 { 70 [super setUp]; 71 // KeychainXCTest already sets up keychain with custom test-named directory 72 } 73 74 - (void)testReturnValuesInSecItemUpdate 75 { 76 NSDictionary* addQuery = @{ (id)kSecClass : (id)kSecClassGenericPassword, 77 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding], 78 (id)kSecAttrAccount : @"TestAccount", 79 (id)kSecAttrService : @"TestService", 80 (id)kSecUseDataProtectionKeychain : @(YES), 81 (id)kSecReturnAttributes : @(YES) 82 }; 83 84 NSDictionary* updateQueryWithNoReturn = @{ (id)kSecClass : (id)kSecClassGenericPassword, 85 (id)kSecAttrAccount : @"TestAccount", 86 (id)kSecAttrService : @"TestService", 87 (id)kSecUseDataProtectionKeychain : @(YES) 88 }; 89 90 CFTypeRef result = NULL; 91 92 // Add the item 93 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)addQuery, &result), errSecSuccess, @"Should have succeeded in adding test item to keychain"); 94 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd"); 95 CFReleaseNull(result); 96 97 // And we can update the item 98 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithNoReturn, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"otherpassword" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with clean update query"); 99 100 // great, a normal update works 101 // now let's do updates with various queries which include return parameters to ensure they succeed on macOS and throw errors on iOS. 102 // this is a status-quo compromise between changing iOS match macOS (which has lamé no-op characteristics) and changing macOS to match iOS, which risks breaking existing clients 103 104 #if TARGET_OS_OSX 105 NSMutableDictionary* updateQueryWithReturnAttributes = updateQueryWithNoReturn.mutableCopy; 106 updateQueryWithReturnAttributes[(id)kSecReturnAttributes] = @(YES); 107 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnAttributes, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-attributes" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with return attributes query"); 108 109 NSMutableDictionary* updateQueryWithReturnData = updateQueryWithNoReturn.mutableCopy; 110 updateQueryWithReturnAttributes[(id)kSecReturnData] = @(YES); 111 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnData, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-data" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with return data query"); 112 113 NSMutableDictionary* updateQueryWithReturnRef = updateQueryWithNoReturn.mutableCopy; 114 updateQueryWithReturnAttributes[(id)kSecReturnRef] = @(YES); 115 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnRef, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-ref" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with return ref query"); 116 117 NSMutableDictionary* updateQueryWithReturnPersistentRef = updateQueryWithNoReturn.mutableCopy; 118 updateQueryWithReturnAttributes[(id)kSecReturnPersistentRef] = @(YES); 119 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnPersistentRef, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-persistent-ref" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with return persistent ref query"); 120 #else 121 NSMutableDictionary* updateQueryWithReturnAttributes = updateQueryWithNoReturn.mutableCopy; 122 updateQueryWithReturnAttributes[(id)kSecReturnAttributes] = @(YES); 123 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnAttributes, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-attributes" dataUsingEncoding:NSUTF8StringEncoding]}), errSecParam, "failed to generate error updating item with return attributes query"); 124 125 NSMutableDictionary* updateQueryWithReturnData = updateQueryWithNoReturn.mutableCopy; 126 updateQueryWithReturnData[(id)kSecReturnData] = @(YES); 127 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnData, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-data" dataUsingEncoding:NSUTF8StringEncoding]}), errSecParam, "failed to generate error updating item with return data query"); 128 129 NSMutableDictionary* updateQueryWithReturnRef = updateQueryWithNoReturn.mutableCopy; 130 updateQueryWithReturnRef[(id)kSecReturnRef] = @(YES); 131 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnRef, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-ref" dataUsingEncoding:NSUTF8StringEncoding]}), errSecParam, "failed to generate error updating item with return ref query"); 132 133 NSMutableDictionary* updateQueryWithReturnPersistentRef = updateQueryWithNoReturn.mutableCopy; 134 updateQueryWithReturnPersistentRef[(id)kSecReturnPersistentRef] = @(YES); 135 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnPersistentRef, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-persistent-ref" dataUsingEncoding:NSUTF8StringEncoding]}), errSecParam, "failed to generate error updating item with return persistent ref query"); 136 #endif 137 } 138 139 - (void)testBadTypeInParams 140 { 141 NSMutableDictionary *attrs = @{ 142 (id)kSecClass: (id)kSecClassGenericPassword, 143 (id)kSecUseDataProtectionKeychain: @YES, 144 (id)kSecAttrLabel: @"testentry", 145 }.mutableCopy; 146 147 SecItemDelete((CFDictionaryRef)attrs); 148 XCTAssertEqual(errSecSuccess, SecItemAdd((CFDictionaryRef)attrs, NULL)); 149 XCTAssertEqual(errSecSuccess, SecItemDelete((CFDictionaryRef)attrs)); 150 151 // We try to fool SecItem API with unexpected type of kSecAttrAccessControl attribute in query and it should not crash. 152 attrs[(id)kSecAttrAccessControl] = @"string, no SecAccessControlRef!"; 153 XCTAssertEqual(errSecParam, SecItemAdd((CFDictionaryRef)attrs, NULL)); 154 XCTAssertEqual(errSecParam, SecItemDelete((CFDictionaryRef)attrs)); 155 } 156 157 - (BOOL)passInternalAttributeToKeychainAPIsWithKey:(id)key value:(id)value { 158 NSDictionary* badquery = @{ 159 (id)kSecClass : (id)kSecClassGenericPassword, 160 (id)kSecAttrService : @"AppClipTestService", 161 (id)kSecUseDataProtectionKeychain : @YES, 162 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding], 163 key : value, 164 }; 165 NSDictionary* badupdate = @{key : value}; 166 167 NSDictionary* okquery = @{ 168 (id)kSecClass : (id)kSecClassGenericPassword, 169 (id)kSecAttrService : @"AppClipTestService", 170 (id)kSecUseDataProtectionKeychain : @YES, 171 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding], 172 }; 173 NSDictionary* okupdate = @{(id)kSecAttrService : @"DifferentService"}; 174 175 if (SecItemAdd((__bridge CFDictionaryRef)badquery, NULL) != errSecParam) { 176 XCTFail("SecItemAdd did not return errSecParam"); 177 return NO; 178 } 179 if (SecItemCopyMatching((__bridge CFDictionaryRef)badquery, NULL) != errSecParam) { 180 XCTFail("SecItemCopyMatching did not return errSecParam"); 181 return NO; 182 } 183 if (SecItemUpdate((__bridge CFDictionaryRef)badquery, (__bridge CFDictionaryRef)okupdate) != errSecParam) { 184 XCTFail("SecItemUpdate with bad query did not return errSecParam"); 185 return NO; 186 } 187 if (SecItemUpdate((__bridge CFDictionaryRef)okquery, (__bridge CFDictionaryRef)badupdate) != errSecParam) { 188 XCTFail("SecItemUpdate with bad update did not return errSecParam"); 189 return NO; 190 } 191 if (SecItemDelete((__bridge CFDictionaryRef)badquery) != errSecParam) { 192 XCTFail("SecItemDelete did not return errSecParam"); 193 return NO; 194 } 195 return YES; 196 } 197 198 // Expand this, rdar://problem/59297616 199 - (void)testNotAllowedToPassInternalAttributes { 200 XCTAssert([self passInternalAttributeToKeychainAPIsWithKey:(__bridge NSString*)kSecAttrAppClipItem value:@YES], @"Expect errSecParam for 'clip' attribute"); 201 } 202 203 #pragma mark - Corruption Tests 204 205 const uint8_t keychain_data[] = { 206 0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xd2, 0x01, 0x02, 0x03, 207 0x04, 0x5f, 0x10, 0x1b, 0x4e, 0x53, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 208 0x20, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x20, 0x50, 0x72, 0x6f, 0x63, 0x65, 209 0x73, 0x73, 0x50, 0x61, 0x6e, 0x65, 0x6c, 0x5f, 0x10, 0x1d, 0x4e, 0x53, 210 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x20, 0x46, 0x72, 0x61, 0x6d, 0x65, 211 0x20, 0x41, 0x62, 0x6f, 0x75, 0x74, 0x20, 0x54, 0x68, 0x69, 0x73, 0x20, 212 0x4d, 0x61, 0x63, 0x5f, 0x10, 0x1c, 0x32, 0x38, 0x20, 0x33, 0x37, 0x33, 213 0x20, 0x33, 0x34, 0x36, 0x20, 0x32, 0x39, 0x30, 0x20, 0x30, 0x20, 0x30, 214 0x20, 0x31, 0x34, 0x34, 0x30, 0x20, 0x38, 0x37, 0x38, 0x20, 0x5f, 0x10, 215 0x1d, 0x35, 0x36, 0x38, 0x20, 0x33, 0x39, 0x35, 0x20, 0x33, 0x30, 0x37, 216 0x20, 0x33, 0x37, 0x39, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x34, 0x34, 217 0x30, 0x20, 0x38, 0x37, 0x38, 0x20, 0x08, 0x0d, 0x2b, 0x4b, 0x6a, 0x00, 218 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 219 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 220 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a 221 }; 222 223 dispatch_semaphore_t sema = NULL; 224 225 // The real corruption exit handler should xpc_transaction_exit_clean, 226 // let's be certain it does not. Also make sure exit handler gets called at all 227 static void SecDbTestCorruptionHandler(void) 228 { 229 dispatch_semaphore_signal(sema); 230 } 231 232 - (void)testCorruptionHandler { 233 __security_simulatecrash_enable(false); 234 235 SecDbCorruptionExitHandler = SecDbTestCorruptionHandler; 236 sema = dispatch_semaphore_create(0); 237 238 // Test teardown will want to delete this keychain. Make sure it knows where to look... 239 NSString* corruptedKeychainPath = [NSString stringWithFormat:@"%@-bad", [self nameOfTest]]; 240 241 if(self.keychainDirectoryPrefix) { 242 XCTAssertTrue(secd_test_teardown_delete_temp_keychain([self.keychainDirectoryPrefix UTF8String]), "Should be able to delete the temp keychain"); 243 } 244 245 self.keychainDirectoryPrefix = corruptedKeychainPath; 246 247 secd_test_setup_temp_keychain([corruptedKeychainPath UTF8String], ^{ 248 CFStringRef keychain_path_cf = __SecKeychainCopyPath(); 249 250 CFStringPerformWithCString(keychain_path_cf, ^(const char *keychain_path) { 251 int fd = open(keychain_path, O_RDWR | O_CREAT | O_TRUNC, 0644); 252 XCTAssert(fd > -1, "Could not open fd to write keychain: %{darwin.errno}d", errno); 253 254 size_t written = write(fd, keychain_data, sizeof(keychain_data)); 255 XCTAssertEqual(written, sizeof(keychain_data), "Write garbage to disk, got %lu instead of %lu: %{darwin.errno}d", written, sizeof(keychain_data), errno); 256 XCTAssertEqual(close(fd), 0, "Close keychain file failed: %{darwin.errno}d", errno); 257 }); 258 259 CFReleaseNull(keychain_path_cf); 260 }); 261 262 NSDictionary* query = @{(id)kSecClass : (id)kSecClassGenericPassword, 263 (id)kSecAttrAccount : @"TestAccount", 264 (id)kSecAttrService : @"TestService", 265 (id)kSecUseDataProtectionKeychain : @(YES), 266 (id)kSecReturnAttributes : @(YES) 267 }; 268 269 CFTypeRef result = NULL; 270 // Real keychain should xpc_transaction_exit_clean() after this, but we nerfed it 271 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)query, &result), errSecNotAvailable, "Expected badness from corrupt keychain"); 272 XCTAssertEqual(dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC)), 0, "Timed out waiting for corruption exit handler"); 273 274 sema = NULL; 275 SecDbResetCorruptionExitHandler(); 276 CFReleaseNull(result); 277 278 NSString* markerpath = [NSString stringWithFormat:@"%@-iscorrupt", CFBridgingRelease(__SecKeychainCopyPath())]; 279 struct stat info = {}; 280 XCTAssertEqual(stat([markerpath UTF8String], &info), 0, "Unable to stat corruption marker: %{darwin.errno}d", errno); 281 } 282 283 - (void)testRecoverFromCorruption { 284 __security_simulatecrash_enable(false); 285 286 // Setup does a reset, but that doesn't create the db yet so let's sneak in first 287 __block struct stat before = {}; 288 WithPathInKeychainDirectory(CFSTR("keychain-2.db"), ^(const char *filename) { 289 FILE* file = fopen(filename, "w"); 290 XCTAssert(file != NULL, "Didn't get a FILE pointer"); 291 fclose(file); 292 XCTAssertEqual(stat(filename, &before), 0, "Unable to stat newly created file"); 293 }); 294 295 WithPathInKeychainDirectory(CFSTR("keychain-2.db-iscorrupt"), ^(const char *filename) { 296 FILE* file = fopen(filename, "w"); 297 XCTAssert(file != NULL, "Didn't get a FILE pointer"); 298 fclose(file); 299 }); 300 301 NSMutableDictionary* query = [@{(id)kSecClass : (id)kSecClassGenericPassword, 302 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding], 303 (id)kSecAttrAccount : @"TestAccount", 304 (id)kSecAttrService : @"TestService", 305 (id)kSecUseDataProtectionKeychain : @(YES), 306 (id)kSecReturnAttributes : @(YES) 307 } mutableCopy]; 308 CFTypeRef result = NULL; 309 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)query, &result), errSecSuccess, @"Should have added item to keychain"); 310 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd"); 311 CFReleaseNull(result); 312 313 query[(id)kSecValueData] = nil; 314 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)query, &result), errSecSuccess, @"Should have found item in keychain"); 315 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemCopyMatching"); 316 CFReleaseNull(result); 317 318 XCTAssertEqual(SecItemDelete((__bridge CFDictionaryRef)query), errSecSuccess, @"Should have deleted item from keychain"); 319 320 WithPathInKeychainDirectory(CFSTR("keychain-2.db-iscorrupt"), ^(const char *filename) { 321 struct stat markerinfo = {}; 322 XCTAssertNotEqual(stat(filename, &markerinfo), 0, "Expected not to find corruption marker after killing keychain"); 323 }); 324 325 __block struct stat after = {}; 326 WithPathInKeychainDirectory(CFSTR("keychain-2.db"), ^(const char *filename) { 327 FILE* file = fopen(filename, "w"); 328 XCTAssert(file != NULL, "Didn't get a FILE pointer"); 329 fclose(file); 330 XCTAssertEqual(stat(filename, &after), 0, "Unable to stat newly created file"); 331 }); 332 333 if (before.st_birthtimespec.tv_sec == after.st_birthtimespec.tv_sec) { 334 XCTAssertLessThan(before.st_birthtimespec.tv_nsec, after.st_birthtimespec.tv_nsec, "db was not deleted and recreated"); 335 } else { 336 XCTAssertLessThan(before.st_birthtimespec.tv_sec, after.st_birthtimespec.tv_sec, "db was not deleted and recreated"); 337 } 338 } 339 340 - (void)testInetBinaryFields { 341 NSData* note = [@"OBVIOUS_NOTES_DATA" dataUsingEncoding:NSUTF8StringEncoding]; 342 NSData* history = [@"OBVIOUS_HISTORY_DATA" dataUsingEncoding:NSUTF8StringEncoding]; 343 NSData* client0 = [@"OBVIOUS_CLIENT0_DATA" dataUsingEncoding:NSUTF8StringEncoding]; 344 NSData* client1 = [@"OBVIOUS_CLIENT1_DATA" dataUsingEncoding:NSUTF8StringEncoding]; 345 NSData* client2 = [@"OBVIOUS_CLIENT2_DATA" dataUsingEncoding:NSUTF8StringEncoding]; 346 NSData* client3 = [@"OBVIOUS_CLIENT3_DATA" dataUsingEncoding:NSUTF8StringEncoding]; 347 348 NSData* originalPassword = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding]; 349 NSMutableDictionary* query = [@{ 350 (id)kSecClass : (id)kSecClassInternetPassword, 351 (id)kSecAttrAccessible : (id)kSecAttrAccessibleWhenUnlocked, 352 (id)kSecUseDataProtectionKeychain : @YES, 353 (id)kSecAttrDescription : @"desc", 354 (id)kSecAttrServer : @"server", 355 (id)kSecAttrAccount : @"test-account", 356 (id)kSecValueData : originalPassword, 357 (id)kSecDataInetExtraNotes : note, 358 (id)kSecDataInetExtraHistory : history, 359 (id)kSecDataInetExtraClientDefined0 : client0, 360 (id)kSecDataInetExtraClientDefined1 : client1, 361 (id)kSecDataInetExtraClientDefined2 : client2, 362 (id)kSecDataInetExtraClientDefined3 : client3, 363 364 (id)kSecReturnAttributes : @YES, 365 } mutableCopy]; 366 367 CFTypeRef cfresult = nil; 368 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)query, &cfresult), errSecSuccess, "Should be able to add an item using new binary fields"); 369 NSDictionary* result = (NSDictionary*)CFBridgingRelease(cfresult); 370 XCTAssertNotNil(result, "Should have some sort of result"); 371 372 XCTAssertNil(result[(id)kSecDataInetExtraNotes], "Notes field should not be returned as an attribute from add"); 373 XCTAssertNil(result[(id)kSecDataInetExtraHistory], "Notes field should not be returned as an attribute from add"); 374 375 NSDictionary* queryFind = @{ 376 (id)kSecClass : (id)kSecClassInternetPassword, 377 (id)kSecUseDataProtectionKeychain : @YES, 378 (id)kSecAttrAccount : @"test-account", 379 }; 380 381 NSMutableDictionary* queryFindOneWithJustAttributes = [[NSMutableDictionary alloc] initWithDictionary:queryFind]; 382 queryFindOneWithJustAttributes[(id)kSecReturnAttributes] = @YES; 383 384 NSMutableDictionary* queryFindAllWithJustAttributes = [[NSMutableDictionary alloc] initWithDictionary:queryFindOneWithJustAttributes]; 385 queryFindAllWithJustAttributes[(id)kSecMatchLimit] = (id)kSecMatchLimitAll; 386 387 NSDictionary* queryFindOneWithAttributesAndData = @{ 388 (id)kSecClass : (id)kSecClassInternetPassword, 389 (id)kSecUseDataProtectionKeychain : @YES, 390 (id)kSecReturnAttributes : @YES, 391 (id)kSecReturnData: @YES, 392 (id)kSecAttrAccount : @"test-account", 393 }; 394 395 NSDictionary* queryFindAllWithAttributesAndData = @{ 396 (id)kSecClass : (id)kSecClassInternetPassword, 397 (id)kSecUseDataProtectionKeychain : @YES, 398 (id)kSecReturnAttributes : @YES, 399 (id)kSecReturnData: @YES, 400 (id)kSecMatchLimit : (id)kSecMatchLimitAll, 401 (id)kSecAttrAccount : @"test-account", 402 }; 403 404 /* Copy with a single record limite, but with attributes only */ 405 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)queryFindOneWithJustAttributes, &cfresult), errSecSuccess, "Should be able to find an item"); 406 407 result = (NSDictionary*)CFBridgingRelease(cfresult); 408 XCTAssertNotNil(result, "Should have some sort of result"); 409 410 XCTAssertNil(result[(id)kSecDataInetExtraNotes], "Notes field should not be returned as an attribute from copymatching when finding a single item"); 411 XCTAssertNil(result[(id)kSecDataInetExtraHistory], "Notes field should not be returned as an attribute from copymatching when finding a single item"); 412 XCTAssertNil(result[(id)kSecDataInetExtraClientDefined0], "ClientDefined0 field should not be returned as an attribute from copymatching when finding a single item"); 413 XCTAssertNil(result[(id)kSecDataInetExtraClientDefined1], "ClientDefined1 field should not be returned as an attribute from copymatching when finding a single item"); 414 XCTAssertNil(result[(id)kSecDataInetExtraClientDefined2], "ClientDefined2 field should not be returned as an attribute from copymatching when finding a single item"); 415 XCTAssertNil(result[(id)kSecDataInetExtraClientDefined3], "ClientDefined3 field should not be returned as an attribute from copymatching when finding a single item"); 416 417 /* Copy with no limit, but with attributes only */ 418 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)queryFindAllWithJustAttributes, &cfresult), errSecSuccess, "Should be able to find an item"); 419 NSArray* arrayResult = (NSArray*)CFBridgingRelease(cfresult); 420 XCTAssertNotNil(arrayResult, "Should have some sort of result"); 421 XCTAssertTrue([arrayResult isKindOfClass:[NSArray class]], "Should have received an array back from copymatching"); 422 XCTAssertEqual(arrayResult.count, 1, "Array should have one element"); 423 424 result = arrayResult[0]; 425 XCTAssertNil(result[(id)kSecDataInetExtraNotes], "Notes field should not be returned as an attribute from copymatching when finding all items"); 426 XCTAssertNil(result[(id)kSecDataInetExtraHistory], "Notes field should not be returned as an attribute from copymatching when finding all items"); 427 XCTAssertNil(result[(id)kSecDataInetExtraClientDefined0], "ClientDefined0 field should not be returned as an attribute from copymatching when finding all items"); 428 XCTAssertNil(result[(id)kSecDataInetExtraClientDefined1], "ClientDefined1 field should not be returned as an attribute from copymatching when finding all items"); 429 XCTAssertNil(result[(id)kSecDataInetExtraClientDefined2], "ClientDefined2 field should not be returned as an attribute from copymatching when finding all items"); 430 XCTAssertNil(result[(id)kSecDataInetExtraClientDefined3], "ClientDefined3 field should not be returned as an attribute from copymatching when finding all items"); 431 432 /* Copy with single-record limit, but with attributes and data */ 433 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)queryFindOneWithAttributesAndData, &cfresult), errSecSuccess, "Should be able to find an item"); 434 result = (NSDictionary*)CFBridgingRelease(cfresult); 435 XCTAssertNotNil(result, "Should have some sort of result"); 436 437 XCTAssertEqualObjects(note, result[(id)kSecDataInetExtraNotes], "Notes field should be returned as data"); 438 XCTAssertEqualObjects(history, result[(id)kSecDataInetExtraHistory], "History field should be returned as data"); 439 XCTAssertEqualObjects(client0, result[(id)kSecDataInetExtraClientDefined0], "Client Defined 0 field should be returned as data"); 440 XCTAssertEqualObjects(client1, result[(id)kSecDataInetExtraClientDefined1], "Client Defined 1 field should be returned as data"); 441 XCTAssertEqualObjects(client2, result[(id)kSecDataInetExtraClientDefined2], "Client Defined 2 field should be returned as data"); 442 XCTAssertEqualObjects(client3, result[(id)kSecDataInetExtraClientDefined3], "Client Defined 3 field should be returned as data"); 443 444 /* Copy with no limit, but with attributes and data */ 445 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)queryFindAllWithAttributesAndData, &cfresult), errSecSuccess, "Should be able to find an item"); 446 arrayResult = (NSArray*)CFBridgingRelease(cfresult); 447 XCTAssertNotNil(arrayResult, "Should have some sort of result"); 448 XCTAssertTrue([arrayResult isKindOfClass:[NSArray class]], "Should have received an array back from copymatching"); 449 XCTAssertEqual(arrayResult.count, 1, "Array should have one element"); 450 result = arrayResult[0]; 451 XCTAssertEqualObjects(note, result[(id)kSecDataInetExtraNotes], "Notes field should be returned as data"); 452 XCTAssertEqualObjects(history, result[(id)kSecDataInetExtraHistory], "History field should be returned as data"); 453 XCTAssertEqualObjects(client0, result[(id)kSecDataInetExtraClientDefined0], "Client Defined 0 field should be returned as data"); 454 XCTAssertEqualObjects(client1, result[(id)kSecDataInetExtraClientDefined1], "Client Defined 1 field should be returned as data"); 455 XCTAssertEqualObjects(client2, result[(id)kSecDataInetExtraClientDefined2], "Client Defined 2 field should be returned as data"); 456 XCTAssertEqualObjects(client3, result[(id)kSecDataInetExtraClientDefined3], "Client Defined 3 field should be returned as data"); 457 458 /* Copy just looking for the password */ 459 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)@{ 460 (id)kSecClass : (id)kSecClassInternetPassword, 461 (id)kSecUseDataProtectionKeychain : @YES, 462 (id)kSecReturnData: @YES, 463 (id)kSecAttrAccount : @"test-account", 464 }, &cfresult), errSecSuccess, "Should be able to find an item"); 465 466 NSData* password = (NSData*)CFBridgingRelease(cfresult); 467 XCTAssertNotNil(password, "Should have some sort of password"); 468 XCTAssertTrue([password isKindOfClass:[NSData class]], "Password is a data"); 469 XCTAssertEqualObjects(originalPassword, password, "Should still be able to fetch the original password"); 470 471 NSData* newHistoryContents = [@"gone" dataUsingEncoding:NSUTF8StringEncoding]; 472 473 NSDictionary* updateQuery = @{ 474 (id)kSecDataInetExtraHistory : newHistoryContents, 475 }; 476 477 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)queryFind, (__bridge CFDictionaryRef)updateQuery), errSecSuccess, "Should be able to update a history field"); 478 479 // And find it again 480 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)queryFindOneWithAttributesAndData, &cfresult), errSecSuccess, "Should be able to find an item"); 481 result = (NSDictionary*)CFBridgingRelease(cfresult); 482 XCTAssertNotNil(result, "Should have some sort of result"); 483 484 XCTAssertEqualObjects(note, result[(id)kSecDataInetExtraNotes], "Notes field should be returned as data"); 485 XCTAssertEqualObjects(newHistoryContents, result[(id)kSecDataInetExtraHistory], "History field should be updated"); 486 XCTAssertEqualObjects(client0, result[(id)kSecDataInetExtraClientDefined0], "Client Defined 0 field should be returned as data"); 487 XCTAssertEqualObjects(client1, result[(id)kSecDataInetExtraClientDefined1], "Client Defined 1 field should be returned as data"); 488 XCTAssertEqualObjects(client2, result[(id)kSecDataInetExtraClientDefined2], "Client Defined 2 field should be returned as data"); 489 XCTAssertEqualObjects(client3, result[(id)kSecDataInetExtraClientDefined3], "Client Defined 3 field should be returned as data"); 490 } 491 492 // When this test starts failing, hopefully rdar://problem/60332379 got fixed 493 - (void)testBadDateCausesDERDecodeValidationError { 494 // Wonky time calculation hastily stolen from SecGregorianDateGetAbsoluteTime and tweaked 495 // As of right now this causes CFCalendarDecomposeAbsoluteTime with Zulu calendar to give a seemingly incorrect date which then causes DER date validation issues 496 CFAbsoluteTime absTime = (CFAbsoluteTime)(((-(1902 * 365) + -38) * 24 + 0) * 60 + -1) * 60 + 1; 497 absTime -= 0.0004; // Just to make sure the nanoseconds keep getting encoded/decoded properly 498 CFDateRef date = CFDateCreate(NULL, absTime); 499 500 CFErrorRef error = NULL; 501 size_t plistSize = der_sizeof_plist(date, &error); 502 XCTAssert(error == NULL); 503 XCTAssertGreaterThan(plistSize, 0); 504 505 // Encode without repair does not validate dates because that changes behavior I do not want to fiddle with 506 uint8_t* der = calloc(1, plistSize); 507 uint8_t* der_end = der + plistSize; 508 uint8_t* result = der_encode_plist(date, &error, der, der_end); 509 XCTAssert(error == NULL); 510 XCTAssertEqual(der, result); 511 512 // ...but decoding does and will complain 513 CFPropertyListRef decoded = NULL; 514 XCTAssert(der_decode_plist(NULL, &decoded, &error, der, der_end) == NULL); 515 XCTAssert(error != NULL); 516 XCTAssertEqual(CFErrorGetDomain(error), kCFErrorDomainOSStatus); 517 NSString* description = CFBridgingRelease(CFErrorCopyDescription(error)); 518 XCTAssert([description containsString:@"Invalid date"]); 519 520 CFReleaseNull(error); 521 free(der); 522 } 523 524 // When this test starts failing, hopefully rdar://problem/60332379 got fixed 525 - (void)testBadDateWithDEREncodingRepairProducesDefaultValue { 526 // Wonky time calculation hastily stolen from SecGregorianDateGetAbsoluteTime and tweaked 527 // As of right now this causes CFCalendarDecomposeAbsoluteTime with Zulu calendar to give a seemingly incorrect date which then causes DER date validation issues 528 CFAbsoluteTime absTime = (CFAbsoluteTime)(((-(1902 * 365) + -38) * 24 + 0) * 60 + -1) * 60 + 1; 529 absTime -= 0.0004; // Just to make sure the nanoseconds keep getting encoded/decoded properly 530 CFDateRef date = CFDateCreate(NULL, absTime); 531 532 CFErrorRef error = NULL; 533 size_t plistSize = der_sizeof_plist(date, &error); 534 XCTAssert(error == NULL); 535 XCTAssertGreaterThan(plistSize, 0); 536 537 uint8_t* der = calloc(1, plistSize); 538 uint8_t* der_end = der + plistSize; 539 uint8_t* encoderesult = der_encode_plist_repair(date, &error, true, der, der_end); 540 XCTAssert(error == NULL); 541 XCTAssertEqual(der, encoderesult); 542 543 CFPropertyListRef decoded = NULL; 544 const uint8_t* decoderesult = der_decode_plist(NULL, &decoded, &error, der, der_end); 545 XCTAssertEqual(der_end, decoderesult); 546 XCTAssertEqual(CFGetTypeID(decoded), CFDateGetTypeID()); 547 XCTAssertEqualWithAccuracy(CFDateGetAbsoluteTime(decoded), 0, 60 * 60 * 24); 548 } 549 550 - (void)testContainersWithBadDateWithDEREncodingRepairProducesDefaultValue { 551 // Wonky time calculation hastily stolen from SecGregorianDateGetAbsoluteTime and tweaked 552 // As of right now this causes CFCalendarDecomposeAbsoluteTime with Zulu calendar to give a seemingly incorrect date which then causes DER date validation issues 553 CFAbsoluteTime absTime = (CFAbsoluteTime)(((-(1902 * 365) + -38) * 24 + 0) * 60 + -1) * 60 + 1; 554 absTime -= 0.0004; 555 CFDateRef date = CFDateCreate(NULL, absTime); 556 557 NSDictionary* dict = @{ 558 @"dateset": [NSSet setWithObject:(__bridge id)date], 559 @"datearray": @[(__bridge id)date], 560 }; 561 562 CFErrorRef error = NULL; 563 size_t plistSize = der_sizeof_plist((__bridge CFTypeRef)dict, &error); 564 XCTAssertNil((__bridge NSError*)error, "Should be no error checking the size of the plist"); 565 XCTAssertGreaterThan(plistSize, 0); 566 567 uint8_t* der = calloc(1, plistSize); 568 uint8_t* der_end = der + plistSize; 569 uint8_t* encoderesult = der_encode_plist_repair((__bridge CFTypeRef)dict, &error, true, der, der_end); 570 XCTAssertNil((__bridge NSError*)error, "Should be no error encoding the plist"); 571 XCTAssertEqual(der, encoderesult); 572 573 CFPropertyListRef decoded = NULL; 574 const uint8_t* decoderesult = der_decode_plist(NULL, &decoded, &error, der, der_end); 575 XCTAssertNil((__bridge NSError*)error, "Should be no error decoding the plist"); 576 XCTAssertEqual(der_end, decoderesult); 577 578 XCTAssertNotNil((__bridge NSDictionary*)decoded, "Should have decoded some dictionary"); 579 if(decoded == nil) { 580 return; 581 } 582 583 XCTAssertEqual(CFGetTypeID(decoded), CFDictionaryGetTypeID()); 584 CFDictionaryRef decodedCFDictionary = decoded; 585 586 { 587 CFSetRef decodedCFSet = CFDictionaryGetValue(decodedCFDictionary, CFSTR("dateset")); 588 XCTAssertNotNil((__bridge NSSet*)decodedCFSet, "Should have some CFSet"); 589 590 if(decodedCFSet != NULL) { 591 XCTAssertEqual(CFGetTypeID(decodedCFSet), CFSetGetTypeID()); 592 XCTAssertEqual(CFSetGetCount(decodedCFSet), 1, "Should have one item in set"); 593 594 __block bool dateprocessed = false; 595 CFSetForEach(decodedCFSet, ^(const void *value) { 596 XCTAssertEqual(CFGetTypeID(value), CFDateGetTypeID()); 597 XCTAssertEqualWithAccuracy(CFDateGetAbsoluteTime(value), 0, 60 * 60 * 24); 598 dateprocessed = true; 599 }); 600 601 XCTAssertTrue(dateprocessed, "Should have processed at least one date in the set"); 602 } 603 } 604 605 { 606 CFArrayRef decodedCFArray = CFDictionaryGetValue(decodedCFDictionary, CFSTR("datearray")); 607 XCTAssertNotNil((__bridge NSArray*)decodedCFArray, "Should have some CFArray"); 608 609 if(decodedCFArray != NULL) { 610 XCTAssertEqual(CFGetTypeID(decodedCFArray), CFArrayGetTypeID()); 611 XCTAssertEqual(CFArrayGetCount(decodedCFArray), 1, "Should have one item in array"); 612 613 __block bool dateprocessed = false; 614 CFArrayForEach(decodedCFArray, ^(const void *value) { 615 XCTAssertEqual(CFGetTypeID(value), CFDateGetTypeID()); 616 XCTAssertEqualWithAccuracy(CFDateGetAbsoluteTime(value), 0, 60 * 60 * 24); 617 dateprocessed = true; 618 }); 619 620 XCTAssertTrue(dateprocessed, "Should have processed at least one date in the array"); 621 } 622 } 623 624 CFReleaseNull(decoded); 625 } 626 627 - (void)testSecItemCopyMatchingWithBadDateInItem { 628 // Wonky time calculation hastily stolen from SecGregorianDateGetAbsoluteTime and tweaked 629 // As of right now this causes CFCalendarDecomposeAbsoluteTime with Zulu calendar to give a seemingly incorrect date which then causes DER date validation issues 630 CFAbsoluteTime absTime = (CFAbsoluteTime)(((-(1902 * 365) + -38) * 24 + 0) * 60 + -1) * 60 + 1; 631 absTime -= 0.0004; 632 CFDateRef date = CFDateCreate(NULL, absTime); 633 634 NSDictionary* addQuery = @{ 635 //(id)kSecClass : (id)kSecClassGenericPassword, 636 (id)kSecAttrCreationDate : (__bridge id)date, 637 (id)kSecAttrModificationDate : (__bridge id)date, 638 639 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding], 640 (id)kSecAttrAccount : @"TestAccount", 641 (id)kSecAttrService : @"TestService", 642 (id)kSecAttrAccessGroup : @"com.apple.security.securityd", 643 644 (id)kSecAttrAccessible : @"ak", 645 (id)kSecAttrTombstone : [NSNumber numberWithInt: 0], 646 (id)kSecAttrMultiUser : [[NSData alloc] init], 647 }; 648 649 __block CFErrorRef cferror = NULL; 650 kc_with_dbt(true, &cferror, ^bool(SecDbConnectionRef dbt) { 651 return kc_transaction_type(dbt, kSecDbExclusiveTransactionType, &cferror, ^bool { 652 SecDbItemRef item = SecDbItemCreateWithAttributes(NULL, kc_class_with_name(kSecClassGenericPassword), (__bridge CFDictionaryRef)addQuery, KEYBAG_DEVICE, &cferror); 653 654 bool ret = SecDbItemInsert(item, dbt, false, &cferror); 655 656 XCTAssertTrue(ret, "Should be able to add an item"); 657 CFReleaseNull(item); 658 659 return ret; 660 }); 661 }); 662 663 NSDictionary* findQuery = @{ 664 (id)kSecClass : (id)kSecClassGenericPassword, 665 (id)kSecUseDataProtectionKeychain : @YES, 666 (id)kSecAttrAccount : @"TestAccount", 667 (id)kSecAttrService : @"TestService", 668 (id)kSecAttrAccessGroup : @"com.apple.security.securityd", 669 670 (id)kSecReturnAttributes : @YES, 671 (id)kSecReturnData: @YES, 672 (id)kSecMatchLimit: (id)kSecMatchLimitAll, 673 }; 674 675 CFTypeRef result = NULL; 676 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), errSecItemNotFound, @"Should not be able to find our misdated item"); 677 678 // This is a bit of a mystery: how do these items have a bad date that's not in the item? 679 680 CFReleaseNull(result); 681 CFReleaseNull(date); 682 } 683 684 - (void)testSecItemCopyMatchingWithBadDateInSQLColumn { 685 NSDictionary* addQuery = @{ 686 (id)kSecClass : (id)kSecClassGenericPassword, 687 688 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding], 689 (id)kSecAttrAccount : @"TestAccount", 690 (id)kSecAttrService : @"TestService", 691 (id)kSecAttrAccessGroup : @"com.apple.security.securityd", 692 693 (id)kSecAttrAccessible : @"ak", 694 (id)kSecAttrService : @"", 695 696 (id)kSecUseDataProtectionKeychain: @YES, 697 }; 698 699 OSStatus status = SecItemAdd((__bridge CFDictionaryRef)addQuery, NULL); 700 XCTAssertEqual(status, errSecSuccess, "Should be able to add an item to the keychain"); 701 702 // Modify the cdat/mdat columns in the keychain db 703 __block CFErrorRef cferror = NULL; 704 kc_with_dbt(true, &cferror, ^bool(SecDbConnectionRef dbt) { 705 return kc_transaction_type(dbt, kSecDbExclusiveTransactionType, &cferror, ^bool { 706 CFErrorRef updateError = NULL; 707 708 // Magic number extracted from testSecItemCopyMatchingWithBadDateInItem 709 SecDbExec(dbt, 710 (__bridge CFStringRef)@"UPDATE genp SET cdat = -59984755259.0004, mdat = -59984755259.0004;", 711 &updateError); 712 713 XCTAssertNil((__bridge NSError*)updateError, "Should be no error updating the table"); 714 CFReleaseNull(updateError); 715 716 return true; 717 }); 718 }); 719 720 // Can we find the item? 721 NSDictionary* findQuery = @{ 722 (id)kSecClass : (id)kSecClassGenericPassword, 723 (id)kSecUseDataProtectionKeychain : @YES, 724 (id)kSecAttrAccount : @"TestAccount", 725 (id)kSecAttrService : @"TestService", 726 (id)kSecAttrAccessGroup : @"com.apple.security.securityd", 727 728 (id)kSecReturnAttributes : @YES, 729 (id)kSecReturnData: @YES, 730 (id)kSecMatchLimit: (id)kSecMatchLimitAll, 731 }; 732 733 CFTypeRef result = NULL; 734 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), errSecSuccess, @"Should be able to find our misdated item"); 735 736 CFReleaseNull(result); 737 } 738 739 - (void)testDurableWriteAPI 740 { 741 NSDictionary* addQuery = @{ 742 (id)kSecClass : (id)kSecClassGenericPassword, 743 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding], 744 (id)kSecAttrAccount : @"TestAccount", 745 (id)kSecAttrService : @"TestService", 746 (id)kSecUseDataProtectionKeychain : @(YES), 747 (id)kSecReturnAttributes : @(YES), 748 }; 749 750 NSDictionary* updateQuery = @{ 751 (id)kSecClass : (id)kSecClassGenericPassword, 752 (id)kSecAttrAccount : @"TestAccount", 753 (id)kSecAttrService : @"TestService", 754 (id)kSecUseDataProtectionKeychain : @(YES), 755 }; 756 757 CFTypeRef result = NULL; 758 759 // Add the item 760 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)addQuery, &result), errSecSuccess, @"Should have succeeded in adding test item to keychain"); 761 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd"); 762 CFReleaseNull(result); 763 764 // Using the API without the entitlement should fail 765 CFErrorRef cferror = NULL; 766 XCTAssertEqual(SecItemPersistKeychainWritesAtHighPerformanceCost(&cferror), errSecMissingEntitlement, @"Should not be able to persist keychain writes without the entitlement"); 767 XCTAssertNotNil((__bridge NSError*)cferror, "Should be an error persisting keychain writes without the entitlement"); 768 CFReleaseNull(cferror); 769 770 // But with the entitlement, you're good 771 SecResetLocalSecuritydXPCFakeEntitlements(); 772 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivatePerformanceImpactingAPI, kCFBooleanTrue); 773 774 XCTAssertEqual(SecItemPersistKeychainWritesAtHighPerformanceCost(&cferror), errSecSuccess, @"Should be able to persist keychain writes"); 775 XCTAssertNil((__bridge NSError*)cferror, "Should be no error persisting keychain writes"); 776 777 // And we can update the item 778 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQuery, 779 (__bridge CFDictionaryRef)@{ 780 (id)kSecValueData: [@"otherpassword" dataUsingEncoding:NSUTF8StringEncoding], 781 }), 782 errSecSuccess, "should be able to update item with clean update query"); 783 784 XCTAssertEqual(SecItemPersistKeychainWritesAtHighPerformanceCost(&cferror), errSecSuccess, @"Should be able to persist keychain writes after an update"); 785 XCTAssertNil((__bridge NSError*)cferror, "Should be no error persisting keychain writes"); 786 787 XCTAssertEqual(SecItemDelete((__bridge CFDictionaryRef)updateQuery), errSecSuccess, "Should be able to delete item"); 788 XCTAssertEqual(SecItemPersistKeychainWritesAtHighPerformanceCost(&cferror), errSecSuccess, @"Should be able to persist keychain writes after a delete"); 789 XCTAssertNil((__bridge NSError*)cferror, "Should be no error persisting keychain writes"); 790 } 791 792 793 #pragma mark - SecItemRateLimit 794 795 // This is not super accurate in BATS, so put some margin around what you need 796 - (void)sleepAlternativeForXCTest:(double)interval 797 { 798 dispatch_semaphore_t localsema = dispatch_semaphore_create(0); 799 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * interval), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ 800 dispatch_semaphore_signal(localsema); 801 }); 802 dispatch_semaphore_wait(localsema, DISPATCH_TIME_FOREVER); 803 } 804 805 - (void)testSecItemRateLimitTimePasses { 806 SecItemRateLimit* rl = [SecItemRateLimit getStaticRateLimit]; 807 [rl forceEnabled: true]; 808 809 for (int idx = 0; idx < rl.roCapacity; ++idx) { 810 XCTAssertTrue(isReadOnlyAPIRateWithinLimits()); 811 } 812 813 for (int idx = 0; idx < rl.rwCapacity; ++idx) { 814 XCTAssertTrue(isModifyingAPIRateWithinLimits()); 815 } 816 817 [self sleepAlternativeForXCTest: 2]; 818 XCTAssertTrue(isReadOnlyAPIRateWithinLimits()); 819 XCTAssertTrue(isModifyingAPIRateWithinLimits()); 820 821 [SecItemRateLimit resetStaticRateLimit]; 822 } 823 824 - (void)testSecItemRateLimitResetAfterExceed { 825 SecItemRateLimit* rl = [SecItemRateLimit getStaticRateLimit]; 826 [rl forceEnabled: true]; 827 828 for (int idx = 0; idx < rl.roCapacity; ++idx) { 829 XCTAssertTrue(isReadOnlyAPIRateWithinLimits()); 830 } 831 XCTAssertFalse(isReadOnlyAPIRateWithinLimits()); 832 XCTAssertTrue(isReadOnlyAPIRateWithinLimits()); 833 834 for (int idx = 0; idx < rl.rwCapacity; ++idx) { 835 XCTAssertTrue(isModifyingAPIRateWithinLimits()); 836 } 837 XCTAssertFalse(isModifyingAPIRateWithinLimits()); 838 XCTAssertTrue(isModifyingAPIRateWithinLimits()); 839 840 [SecItemRateLimit resetStaticRateLimit]; 841 } 842 843 - (void)testSecItemRateLimitMultiplier { 844 SecItemRateLimit* rl = [SecItemRateLimit getStaticRateLimit]; 845 [rl forceEnabled: true]; 846 847 int ro_iterations_before = 0; 848 for (; ro_iterations_before < rl.roCapacity; ++ro_iterations_before) { 849 XCTAssertTrue(isReadOnlyAPIRateWithinLimits()); 850 } 851 XCTAssertFalse(isReadOnlyAPIRateWithinLimits()); 852 853 int rw_iterations_before = 0; 854 for (; rw_iterations_before < rl.rwCapacity; ++rw_iterations_before) { 855 XCTAssertTrue(isModifyingAPIRateWithinLimits()); 856 } 857 XCTAssertFalse(isModifyingAPIRateWithinLimits()); 858 859 860 int ro_iterations_after = 0; 861 for (; ro_iterations_after < rl.roCapacity; ++ro_iterations_after) { 862 XCTAssertTrue(isReadOnlyAPIRateWithinLimits()); 863 } 864 XCTAssertFalse(isReadOnlyAPIRateWithinLimits()); 865 866 int rw_iterations_after = 0; 867 for (; rw_iterations_after < rl.rwCapacity; ++rw_iterations_after) { 868 XCTAssertTrue(isModifyingAPIRateWithinLimits()); 869 } 870 XCTAssertFalse(isModifyingAPIRateWithinLimits()); 871 872 XCTAssertEqualWithAccuracy(rl.limitMultiplier * ro_iterations_before, ro_iterations_after, 1); 873 XCTAssertEqualWithAccuracy(rl.limitMultiplier * rw_iterations_before, rw_iterations_after, 1); 874 [SecItemRateLimit resetStaticRateLimit]; 875 } 876 877 // We stipulate that this test is run on an internal release. 878 // If this were a platform binary limits would be enforced, but it should not be so they should not. 879 - (void)testSecItemRateLimitInternalPlatformBinariesOnly { 880 SecItemRateLimit* rl = [SecItemRateLimit getStaticRateLimit]; 881 882 for (int idx = 0; idx < 3 * MAX(rl.roCapacity, rl.rwCapacity); ++idx) { 883 XCTAssertTrue(isReadOnlyAPIRateWithinLimits()); 884 XCTAssertTrue(isModifyingAPIRateWithinLimits()); 885 } 886 887 [SecItemRateLimit resetStaticRateLimit]; 888 } 889 890 @end 891 892 #endif