2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
26 #import <CloudKit/CloudKit.h>
27 #import <XCTest/XCTest.h>
28 #import <OCMock/OCMock.h>
30 #include <Security/SecItemPriv.h>
31 #include <Security/SecEntitlements.h>
32 #include <ipc/server_security_helpers.h>
34 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
35 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
36 #import "keychain/ckks/CKKS.h"
37 #import "keychain/ckks/CKKSCurrentItemPointer.h"
38 #import "keychain/ckks/CKKSMirrorEntry.h"
40 #import "keychain/ckks/tests/MockCloudKit.h"
41 #import "keychain/ckks/tests/AutoreleaseTest.h"
43 #import "keychain/ckks/tests/CKKSTests.h"
44 #import "keychain/ckks/tests/CKKSTests+API.h"
46 @interface CloudKitKeychainSyncingCurrentPointerAPITests : CloudKitKeychainSyncingTestsBase
49 @implementation CloudKitKeychainSyncingCurrentPointerAPITests
51 -(void)fetchCurrentPointer:(bool)cached persistentRef:(NSData*)persistentRef
53 XCTestExpectation* currentExpectation = [self expectationWithDescription: @"callback occurs"];
54 SecItemFetchCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
55 (__bridge CFStringRef)@"pcsservice",
56 (__bridge CFStringRef)@"keychain",
58 ^(CFDataRef currentPersistentRef, CFErrorRef cferror) {
59 XCTAssertNotNil((__bridge id)currentPersistentRef, "current item exists");
60 XCTAssertNil((__bridge id)cferror, "no error exists when there's a current item");
61 XCTAssertEqualObjects(persistentRef, (__bridge id)currentPersistentRef, "persistent ref matches expected persistent ref");
62 [currentExpectation fulfill];
64 [self waitForExpectationsWithTimeout:8.0 handler:nil];
66 -(void)fetchCurrentPointerExpectingError:(bool)fetchCloudValue
68 XCTestExpectation* currentExpectation = [self expectationWithDescription: @"callback occurs"];
69 TEST_API_AUTORELEASE_BEFORE(SecItemFetchCurrentItemAcrossAllDevices);
70 SecItemFetchCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
71 (__bridge CFStringRef)@"pcsservice",
72 (__bridge CFStringRef)@"keychain",
74 ^(CFDataRef currentPersistentRef, CFErrorRef cferror) {
75 XCTAssertNil((__bridge id)currentPersistentRef, "no current item exists");
76 XCTAssertNotNil((__bridge id)cferror, "Error exists when there's a current item");
77 [currentExpectation fulfill];
79 TEST_API_AUTORELEASE_AFTER(SecItemFetchCurrentItemAcrossAllDevices);
80 [self waitForExpectationsWithTimeout:8.0 handler:nil];
83 - (void)testPCSFetchCurrentPointerCachedAndUncached {
84 SecResetLocalSecuritydXPCFakeEntitlements();
85 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
86 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
87 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
89 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
90 [self startCKKSSubsystem];
92 [self.keychainView waitForKeyHierarchyReadiness];
93 [self.keychainView waitUntilAllOperationsAreFinished]; // ensure everything finishes before we disallow fetches
95 // Ensure that local queries don't hit the server.
96 self.silentFetchesAllowed = false;
97 [self fetchCurrentPointerExpectingError:false];
99 // And ensure that global queries do.
100 [self expectCKFetch];
101 [self fetchCurrentPointerExpectingError:true];
102 OCMVerifyAllWithDelay(self.mockDatabase, 20);
103 SecResetLocalSecuritydXPCFakeEntitlements();
106 - (void)testPCSCurrentPointerAddAndUpdate {
107 SecResetLocalSecuritydXPCFakeEntitlements();
108 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
109 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
110 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
112 NSNumber* servIdentifier = @3;
113 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
114 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
116 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
117 [self startCKKSSubsystem];
119 // Let things shake themselves out.
120 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"key state should enter 'ready'");
121 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
123 // Ensure there's no current pointer
124 [self fetchCurrentPointerExpectingError:false];
126 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
128 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
129 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
130 PCSServiceIdentifier:(NSNumber *)servIdentifier
131 PCSPublicKey:publicKey
132 PCSPublicIdentity:publicIdentity]];
134 NSDictionary* result = [self pcsAddItem:@"testaccount"
135 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
136 serviceIdentifier:(NSNumber*)servIdentifier
137 publicKey:(NSData*)publicKey
138 publicIdentity:(NSData*)publicIdentity
140 XCTAssertNotNil(result, "Received result from adding item");
141 [self waitForExpectations:@[keychainChanged] timeout:8];
143 // Check that the record is where we expect it in CloudKit
144 [self waitForCKModifications];
145 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
146 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
147 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
149 NSData* persistentRef = result[(id)kSecValuePersistentRef];
150 NSData* sha1 = result[(id)kSecAttrSHA1];
152 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
153 deletedRecordTypeCounts:nil
154 zoneID:self.keychainZoneID
155 checkModifiedRecord:nil
156 runAfterModification:nil];
158 // Set the 'current' pointer.
159 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
161 // Ensure that setting the current pointer sends a notification
162 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
164 TEST_API_AUTORELEASE_BEFORE(SecItemSetCurrentItemAcrossAllDevices);
165 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
166 (__bridge CFStringRef)@"pcsservice",
167 (__bridge CFStringRef)@"keychain",
168 (__bridge CFDataRef)persistentRef,
169 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
170 NSError* error = (__bridge NSError*)cferror;
171 XCTAssertNil(error, "No error setting current item");
172 [setCurrentExpectation fulfill];
174 TEST_API_AUTORELEASE_AFTER(SecItemSetCurrentItemAcrossAllDevices);
175 OCMVerifyAllWithDelay(self.mockDatabase, 20);
176 [self waitForExpectations:@[keychainChanged] timeout:8];
177 [self waitForCKModifications];
179 [self waitForExpectationsWithTimeout:8.0 handler:nil];
181 CKRecord* currentItemPointer = self.keychainZone.currentDatabase[[[CKRecordID alloc] initWithRecordName:@"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID]];
182 XCTAssertNotNil(currentItemPointer, "Found a CKRecord at the expected location in CloudKit");
183 XCTAssertEqualObjects(currentItemPointer.recordType, SecCKRecordCurrentItemType, "Saved CKRecord is correct type");
184 XCTAssertEqualObjects(((CKReference*)currentItemPointer[SecCKRecordItemRefKey]).recordID, pcsItemRecordID, "Current Item record points to correct record");
186 // Check that the status APIs return the right value
187 [self fetchCurrentPointer:false persistentRef:persistentRef];
189 // Rad. If we got here, adding a new current item pointer works. Let's see if we can modify one.
190 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
192 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
193 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
194 PCSServiceIdentifier:(NSNumber *)servIdentifier
195 PCSPublicKey:publicKey
196 PCSPublicIdentity:publicIdentity]];
198 result = [self pcsAddItem:@"tOTHER-ITEM"
199 data:[@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding]
200 serviceIdentifier:(NSNumber*)servIdentifier
201 publicKey:(NSData*)publicKey
202 publicIdentity:(NSData*)publicIdentity
204 XCTAssertNotNil(result, "Received result from adding item");
205 [self waitForExpectations:@[keychainChanged] timeout:8];
207 // Check that the record is where we expect it
208 [self waitForCKModifications];
209 CKRecordID* pcsOtherItemRecordID = [[CKRecordID alloc] initWithRecordName: @"878BEAA6-1EE9-1079-1025-E6832AC8F2F3" zoneID:self.keychainZoneID];
210 CKRecord* recordOther = self.keychainZone.currentDatabase[pcsOtherItemRecordID];
211 XCTAssertNotNil(recordOther, "Found other record in CloudKit at expected UUID");
213 NSData* otherPersistentRef = result[(id)kSecValuePersistentRef];
214 NSData* otherSha1 = result[(id)kSecAttrSHA1];
216 // change the 'current' pointer.
218 // Refetch the old item's hash, just in case it's changed (it does, about 50% of the time. I'm not sure why).
219 CFTypeRef cfresult = NULL;
220 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) @{
221 (id)kSecValuePersistentRef : persistentRef,
222 (id)kSecReturnAttributes : @YES,
223 }, &cfresult), "Found original item by persistent reference");
225 XCTAssertNotNil((__bridge id)cfresult, "Received an item by finding persistent reference");
226 NSData* actualSHA1 = CFBridgingRelease(CFRetainSafe(CFDictionaryGetValue(cfresult, kSecAttrSHA1)));
227 XCTAssertNotNil(actualSHA1, "Have a SHA1 for the original item");
228 CFReleaseNull(cfresult);
230 if(![actualSHA1 isEqual:sha1]) {
231 secnotice("ckks", "SHA1s don't match, but why?");
234 XCTestExpectation* otherSetCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
236 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
237 deletedRecordTypeCounts:nil
238 zoneID:self.keychainZoneID
239 checkModifiedRecord:nil
240 runAfterModification:nil];
242 // Ensure that setting the current pointer sends a notification
243 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
245 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
246 (__bridge CFStringRef)@"pcsservice",
247 (__bridge CFStringRef)@"keychain",
248 (__bridge CFDataRef)otherPersistentRef,
249 (__bridge CFDataRef)otherSha1,
250 (__bridge CFDataRef)persistentRef,
251 (__bridge CFDataRef)actualSHA1, ^ (CFErrorRef cferror) {
252 NSError* error = (__bridge NSError*)cferror;
253 XCTAssertNil(error, "No error setting current item");
254 [otherSetCurrentExpectation fulfill];
256 OCMVerifyAllWithDelay(self.mockDatabase, 20);
257 [self waitForExpectations:@[keychainChanged] timeout:8];
258 [self waitForCKModifications];
260 [self waitForExpectationsWithTimeout:8.0 handler:nil];
262 currentItemPointer = self.keychainZone.currentDatabase[[[CKRecordID alloc] initWithRecordName:@"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID]];
263 XCTAssertNotNil(currentItemPointer, "Found a CKRecord at the expected location in CloudKit");
264 XCTAssertEqualObjects(currentItemPointer.recordType, SecCKRecordCurrentItemType, "Saved CKRecord is correct type");
265 XCTAssertEqualObjects(((CKReference*)currentItemPointer[SecCKRecordItemRefKey]).recordID, pcsOtherItemRecordID, "Current Item record points to updated record");
268 [self fetchCurrentPointer:false persistentRef:otherPersistentRef];
269 [self fetchCurrentPointer:true persistentRef:otherPersistentRef];
271 SecResetLocalSecuritydXPCFakeEntitlements();
274 - (void)testPCSCurrentPointerAddMissingItem {
275 SecResetLocalSecuritydXPCFakeEntitlements();
276 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
277 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
278 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
280 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
281 [self startCKKSSubsystem];
283 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
285 [self fetchCurrentPointerExpectingError:false];
287 NSData* fakepersistentRef = [@"not a real pref" dataUsingEncoding:NSUTF8StringEncoding];
288 NSData* fakesha1 = [@"not a real sha1" dataUsingEncoding:NSUTF8StringEncoding];
290 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
292 TEST_API_AUTORELEASE_BEFORE(SecItemSetCurrentItemAcrossAllDevices);
293 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
294 (__bridge CFStringRef)@"pcsservice",
295 (__bridge CFStringRef)@"keychain",
296 (__bridge CFDataRef)fakepersistentRef,
297 (__bridge CFDataRef)fakesha1, NULL, NULL, ^ (CFErrorRef cferror) {
298 NSError* error = (__bridge NSError*)cferror;
299 XCTAssertNotNil(error, "Should error setting current item to a nonexistent item");
300 [setCurrentExpectation fulfill];
302 TEST_API_AUTORELEASE_AFTER(SecItemSetCurrentItemAcrossAllDevices);
303 OCMVerifyAllWithDelay(self.mockDatabase, 20);
304 [self waitForCKModifications];
306 [self waitForExpectationsWithTimeout:8.0 handler:nil];
308 SecResetLocalSecuritydXPCFakeEntitlements();
311 - (void)testPCSCurrentPointerAddMissingOldItem {
312 SecResetLocalSecuritydXPCFakeEntitlements();
313 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
314 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
315 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
317 NSNumber* servIdentifier = @3;
318 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
319 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
321 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
322 [self startCKKSSubsystem];
324 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
326 [self fetchCurrentPointerExpectingError:false];
328 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
330 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
331 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
332 PCSServiceIdentifier:(NSNumber *)servIdentifier
333 PCSPublicKey:publicKey
334 PCSPublicIdentity:publicIdentity]];
336 NSDictionary* result = [self pcsAddItem:@"testaccount"
337 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
338 serviceIdentifier:(NSNumber*)servIdentifier
339 publicKey:(NSData*)publicKey
340 publicIdentity:(NSData*)publicIdentity
342 XCTAssertNotNil(result, "Received result from adding item");
343 [self waitForExpectations:@[keychainChanged] timeout:8];
345 // Check that the record is where we expect it in CloudKit
346 [self waitForCKModifications];
347 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
348 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
349 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
351 NSData* persistentRef = result[(id)kSecValuePersistentRef];
352 NSData* sha1 = result[(id)kSecAttrSHA1];
354 // Set the 'current' pointer.
355 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
357 NSData* fakepersistentRef = [@"not a real pref" dataUsingEncoding:NSUTF8StringEncoding];
358 NSData* fakesha1 = [@"not a real sha1" dataUsingEncoding:NSUTF8StringEncoding];
360 TEST_API_AUTORELEASE_BEFORE(SecItemSetCurrentItemAcrossAllDevices);
361 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
362 (__bridge CFStringRef)@"pcsservice",
363 (__bridge CFStringRef)@"keychain",
364 (__bridge CFDataRef)persistentRef,
365 (__bridge CFDataRef)sha1,
366 (__bridge CFDataRef)fakepersistentRef,
367 (__bridge CFDataRef)fakesha1,
368 ^(CFErrorRef cferror) {
369 NSError* error = (__bridge NSError*)cferror;
370 XCTAssertNotNil(error, "Should error setting current item when passing garbage for old item");
371 [setCurrentExpectation fulfill];
373 TEST_API_AUTORELEASE_AFTER(SecItemSetCurrentItemAcrossAllDevices);
374 OCMVerifyAllWithDelay(self.mockDatabase, 20);
375 [self waitForCKModifications];
377 [self waitForExpectations:@[setCurrentExpectation] timeout:8];
379 SecResetLocalSecuritydXPCFakeEntitlements();
382 - (void)testPCSCurrentPointerAddNoCloudKitAccount {
383 SecResetLocalSecuritydXPCFakeEntitlements();
384 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
385 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
386 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
388 NSNumber* servIdentifier = @3;
389 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
390 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
392 // Entirely signed out of iCloud. all current record writes should fail.
393 self.accountStatus = CKAccountStatusNoAccount;
395 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
396 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
398 self.silentFetchesAllowed = false;
399 [self startCKKSSubsystem];
401 // Ensure there's no current pointer
402 [self fetchCurrentPointerExpectingError:false];
404 // Should NOT add an item to CloudKit
405 NSDictionary* result = [self pcsAddItem:@"testaccount"
406 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
407 serviceIdentifier:(NSNumber*)servIdentifier
408 publicKey:(NSData*)publicKey
409 publicIdentity:(NSData*)publicIdentity
410 expectingSync:false];
411 XCTAssertNotNil(result, "Received result from adding item");
413 NSData* persistentRef = result[(id)kSecValuePersistentRef];
414 NSData* sha1 = result[(id)kSecAttrSHA1];
416 // Set the 'current' pointer. This should fail.
417 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
419 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
420 (__bridge CFStringRef)@"pcsservice",
421 (__bridge CFStringRef)@"keychain",
422 (__bridge CFDataRef)persistentRef,
423 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
424 NSError* error = (__bridge NSError*)cferror;
425 XCTAssertNotNil(error, "Error setting current item with no CloudKit account");
426 [setCurrentExpectation fulfill];
429 [self waitForExpectationsWithTimeout:8.0 handler:nil];
430 SecResetLocalSecuritydXPCFakeEntitlements();
433 - (void)testPCSCurrentPointerAddNonSyncItem {
434 SecResetLocalSecuritydXPCFakeEntitlements();
435 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
436 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
437 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
439 NSNumber* servIdentifier = @3;
440 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
441 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
443 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
444 [self startCKKSSubsystem];
445 [self.keychainView waitForKeyHierarchyReadiness];
447 NSMutableDictionary* query = [self pcsAddItemQuery:@"account"
448 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
449 serviceIdentifier:servIdentifier
451 publicIdentity:publicIdentity];
452 query[(id)kSecAttrSynchronizable] = @NO;
454 CFTypeRef cfresult = NULL;
455 XCTestExpectation* syncExpectation = [self expectationWithDescription: @"_SecItemAddAndNotifyOnSync callback occured"];
457 // Note that you will NOT receive a notification here, since you're adding a nonsync item
458 syncExpectation.inverted = YES;
460 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, &cfresult, ^(bool didSync, CFErrorRef error) {
461 XCTAssertFalse(didSync, "Item did not sync");
462 XCTAssertNotNil((__bridge NSError*)error, "Error syncing item");
464 [syncExpectation fulfill];
465 }), @"_SecItemAddAndNotifyOnSync succeeded");
467 // We don't expect this callback to fire, so give it a second or so
468 [self waitForExpectations:@[syncExpectation] timeout:20];
470 NSDictionary* result = CFBridgingRelease(cfresult);
472 XCTAssertNotNil(result, "Received result from adding item");
474 NSData* persistentRef = result[(id)kSecValuePersistentRef];
475 NSData* sha1 = result[(id)kSecAttrSHA1];
477 // Set the 'current' pointer. This should fail.
478 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
480 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
481 (__bridge CFStringRef)@"pcsservice",
482 (__bridge CFStringRef)@"keychain",
483 (__bridge CFDataRef)persistentRef,
484 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
485 NSError* error = (__bridge NSError*)cferror;
486 XCTAssertNotNil(error, "Error setting current item to nonsyncable item");
487 [setCurrentExpectation fulfill];
490 [self waitForExpectations:@[setCurrentExpectation] timeout:20];
491 SecResetLocalSecuritydXPCFakeEntitlements();
494 - (void)testPCSCurrentPointerReceive {
495 SecResetLocalSecuritydXPCFakeEntitlements();
496 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
497 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
498 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
500 NSNumber* servIdentifier = @3;
501 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
502 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
504 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
505 [self startCKKSSubsystem];
507 // Let things shake themselves out.
508 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"key state should enter 'ready'");
509 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
511 // Ensure there's no current pointer
512 [self fetchCurrentPointerExpectingError:false];
514 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
515 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
516 PCSServiceIdentifier:(NSNumber *)servIdentifier
517 PCSPublicKey:publicKey
518 PCSPublicIdentity:publicIdentity]];
520 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
522 NSDictionary* result = [self pcsAddItem:@"testaccount"
523 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
524 serviceIdentifier:(NSNumber*)servIdentifier
525 publicKey:(NSData*)publicKey
526 publicIdentity:(NSData*)publicIdentity
528 XCTAssertNotNil(result, "Received result from adding item");
529 NSData* persistentRef = result[(id)kSecValuePersistentRef];
531 [self waitForExpectations:@[keychainChanged] timeout:8];
534 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
535 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
536 PCSServiceIdentifier:(NSNumber *)servIdentifier
537 PCSPublicKey:publicKey
538 PCSPublicIdentity:publicIdentity]];
539 result = [self pcsAddItem:@"testaccount2"
540 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
541 serviceIdentifier:(NSNumber*)servIdentifier
542 publicKey:(NSData*)publicKey
543 publicIdentity:(NSData*)publicIdentity
545 XCTAssertNotNil(result, "Received result from adding item");
546 NSData* persistentRef2 = result[(id)kSecValuePersistentRef];
548 // Check that the records are where we expect them in CloudKit
549 [self waitForCKModifications];
550 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
551 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
552 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
554 CKRecordID* pcsItemRecordID2 = [[CKRecordID alloc] initWithRecordName: @"3AB8E78D-75AF-CFEF-F833-FA3E3E90978A" zoneID:self.keychainZoneID];
555 CKRecord* record2 = self.keychainZone.currentDatabase[pcsItemRecordID2];
556 XCTAssertNotNil(record2, "Found 2nd record in CloudKit at expected UUID");
558 // Still no current pointer.
559 [self fetchCurrentPointerExpectingError:false];
561 // Another machine comes along and updates the pointer!
562 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
563 currentItemUUID:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3"
564 state:SecCKKSProcessedStateRemote
565 zoneID:self.keychainZoneID
566 encodedCKRecord:nil];
567 [self.keychainZone addToZone: [cip CKRecordWithZoneID:self.keychainZoneID]];
568 CKRecordID* currentPointerRecordID = [[CKRecordID alloc] initWithRecordName: @"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID];
569 CKRecord* currentPointerRecord = self.keychainZone.currentDatabase[currentPointerRecordID];
570 XCTAssertNotNil(currentPointerRecord, "Found record in CloudKit at expected UUID");
572 // Ensure that receiving the current item pointer generates a notification
573 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
575 [self.keychainView notifyZoneChange:nil];
576 [self.keychainView waitForFetchAndIncomingQueueProcessing];
578 [self waitForExpectations:@[keychainChanged] timeout:8];
579 [self fetchCurrentPointer:false persistentRef:persistentRef];
582 CKKSCurrentItemPointer* cip2 = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
583 currentItemUUID:pcsItemRecordID2.recordName
584 state:SecCKKSProcessedStateRemote
585 zoneID:self.keychainZoneID
586 encodedCKRecord:nil];
587 [self.keychainZone addToZone: [cip2 CKRecordWithZoneID:self.keychainZoneID]];
588 CKRecordID* currentPointerRecordID2 = [[CKRecordID alloc] initWithRecordName: @"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID];
589 CKRecord* currentPointerRecord2 = self.keychainZone.currentDatabase[currentPointerRecordID2];
590 XCTAssertNotNil(currentPointerRecord2, "Found record in CloudKit at expected UUID");
592 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
594 [self.keychainView notifyZoneChange:nil];
595 [self.keychainView waitForFetchAndIncomingQueueProcessing];
597 [self waitForExpectations:@[keychainChanged] timeout:8];
598 [self fetchCurrentPointer:false persistentRef:persistentRef2];
600 SecResetLocalSecuritydXPCFakeEntitlements();
604 - (void)testPCSCurrentPointerReceiveDelete {
605 SecResetLocalSecuritydXPCFakeEntitlements();
606 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
607 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
608 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
610 NSNumber* servIdentifier = @3;
611 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
612 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
614 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
615 [self startCKKSSubsystem];
617 // Let things shake themselves out.
618 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"key state should enter 'ready'");
619 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
621 // Ensure there's no current pointer
622 [self fetchCurrentPointerExpectingError:false];
624 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
625 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
626 PCSServiceIdentifier:(NSNumber *)servIdentifier
627 PCSPublicKey:publicKey
628 PCSPublicIdentity:publicIdentity]];
630 NSDictionary* result = [self pcsAddItem:@"testaccount"
631 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
632 serviceIdentifier:(NSNumber*)servIdentifier
633 publicKey:(NSData*)publicKey
634 publicIdentity:(NSData*)publicIdentity
636 XCTAssertNotNil(result, "Received result from adding item");
637 NSData* persistentRef = result[(id)kSecValuePersistentRef];
639 // Check that the record is where we expect it in CloudKit
640 [self waitForCKModifications];
641 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
642 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
643 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
645 // Still no current pointer.
646 [self fetchCurrentPointerExpectingError:false];
648 // Another machine comes along and updates the pointer!
649 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
650 currentItemUUID:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3"
651 state:SecCKKSProcessedStateRemote
652 zoneID:self.keychainZoneID
653 encodedCKRecord:nil];
654 [self.keychainZone addToZone: [cip CKRecordWithZoneID:self.keychainZoneID]];
655 CKRecordID* currentPointerRecordID = [[CKRecordID alloc] initWithRecordName: @"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID];
656 CKRecord* currentPointerRecord = self.keychainZone.currentDatabase[currentPointerRecordID];
657 XCTAssertNotNil(currentPointerRecord, "Found record in CloudKit at expected UUID");
659 [self.keychainView notifyZoneChange:nil];
660 [self.keychainView waitForFetchAndIncomingQueueProcessing];
662 [self fetchCurrentPointer:false persistentRef:persistentRef];
664 // Ensure that receiving the current item pointer generates a notification
665 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
667 // Another machine comes along and deletes the pointer!
668 [self.keychainZone deleteCKRecordIDFromZone: currentPointerRecordID];
669 [self.keychainView notifyZoneChange:nil];
670 [self.keychainView waitForFetchAndIncomingQueueProcessing];
671 [self waitForExpectations:@[keychainChanged] timeout:8];
673 [self fetchCurrentPointerExpectingError:false];
675 SecResetLocalSecuritydXPCFakeEntitlements();
679 - (void)testPCSCurrentPointerRecoverFromRecordExistsError {
680 SecResetLocalSecuritydXPCFakeEntitlements();
681 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
682 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
683 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
685 NSNumber* servIdentifier = @3;
686 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
687 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
689 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
690 [self startCKKSSubsystem];
692 // Let things shake themselves out.
693 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"key state should enter 'ready'");
694 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
696 // Ensure there's no current pointer
697 [self fetchCurrentPointerExpectingError:false];
699 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
700 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
701 PCSServiceIdentifier:(NSNumber *)servIdentifier
702 PCSPublicKey:publicKey
703 PCSPublicIdentity:publicIdentity]];
705 NSDictionary* result = [self pcsAddItem:@"testaccount"
706 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
707 serviceIdentifier:(NSNumber*)servIdentifier
708 publicKey:(NSData*)publicKey
709 publicIdentity:(NSData*)publicIdentity
711 XCTAssertNotNil(result, "Received result from adding item");
713 // Check that the record is where we expect it in CloudKit
714 [self waitForCKModifications];
715 NSString* recordUUID = @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3";
716 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName:recordUUID zoneID:self.keychainZoneID];
717 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
718 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
720 // Someone else sets the current record pointer
721 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
722 currentItemUUID:recordUUID
723 state:SecCKKSProcessedStateRemote
724 zoneID:self.keychainZoneID
725 encodedCKRecord:nil];
726 XCTAssertNotNil(cip, "Should have created a CIP");
727 CKRecord* cipRecord = [cip CKRecordWithZoneID:self.keychainZoneID];
728 XCTAssertNotNil(cipRecord, "Should have created a CKRecord for this CIP");
729 [self.keychainZone addToZone: cipRecord];
731 NSData* persistentRef = result[(id)kSecValuePersistentRef];
732 NSData* sha1 = result[(id)kSecAttrSHA1];
734 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
736 // Set the 'current' pointer.
737 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
739 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
740 (__bridge CFStringRef)@"pcsservice",
741 (__bridge CFStringRef)@"keychain",
742 (__bridge CFDataRef)persistentRef,
743 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
744 NSError* error = (__bridge NSError*)cferror;
745 XCTAssertNotNil(error, "Should have received an error setting current item (because of conflict)");
746 [setCurrentExpectation fulfill];
748 OCMVerifyAllWithDelay(self.mockDatabase, 20);
749 [self waitForCKModifications];
750 [self waitForExpectationsWithTimeout:8.0 handler:nil];
752 [self.keychainView waitUntilAllOperationsAreFinished];
754 [self fetchCurrentPointer:false persistentRef:persistentRef];
756 SecResetLocalSecuritydXPCFakeEntitlements();
759 - (void)testPCSCurrentPointerWasCurrent {
760 SecResetLocalSecuritydXPCFakeEntitlements();
761 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
762 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
763 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
765 NSNumber* servIdentifier = @3;
766 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
767 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
769 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
770 [self startCKKSSubsystem];
772 // Let things shake themselves out.
773 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"key state should enter 'ready'");
774 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
776 // Ensure there's no current pointer
777 [self fetchCurrentPointerExpectingError:false];
779 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
780 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
781 PCSServiceIdentifier:(NSNumber *)servIdentifier
782 PCSPublicKey:publicKey
783 PCSPublicIdentity:publicIdentity]];
785 NSDictionary* result = [self pcsAddItem:@"testaccount"
786 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
787 serviceIdentifier:(NSNumber*)servIdentifier
788 publicKey:(NSData*)publicKey
789 publicIdentity:(NSData*)publicIdentity
791 XCTAssertNotNil(result, "Received result from adding item");
793 // Check that the record is where we expect it in CloudKit
794 [self waitForCKModifications];
795 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
796 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
797 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
799 NSData* persistentRef = result[(id)kSecValuePersistentRef];
800 NSData* sha1 = result[(id)kSecAttrSHA1];
802 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
803 deletedRecordTypeCounts:nil
804 zoneID:self.keychainZoneID
805 checkModifiedRecord:nil
806 runAfterModification:nil];
808 // Set the 'current' pointer.
809 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
811 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
812 (__bridge CFStringRef)@"pcsservice",
813 (__bridge CFStringRef)@"keychain",
814 (__bridge CFDataRef)persistentRef,
815 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
816 NSError* error = (__bridge NSError*)cferror;
817 XCTAssertNil(error, "No error setting current item");
818 [setCurrentExpectation fulfill];
820 OCMVerifyAllWithDelay(self.mockDatabase, 20);
821 [self waitForExpectationsWithTimeout:8.0 handler:nil];
822 [self waitForCKModifications];
824 // Set the 'was current' flag on the record
825 CKRecord* modifiedRecord = [record copy];
826 modifiedRecord[SecCKRecordServerWasCurrent] = [NSNumber numberWithInteger:10];
827 [self.keychainZone addToZone:modifiedRecord];
829 [self.keychainView notifyZoneChange:nil];
830 [self.keychainView waitForFetchAndIncomingQueueProcessing];
832 // Check that the number is on the CKKSMirrorEntry
833 [self.keychainView dispatchSync: ^bool {
834 NSError* error = nil;
835 CKKSMirrorEntry* ckme = [CKKSMirrorEntry fromDatabase:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID error:&error];
837 XCTAssertNil(error, "no error fetching ckme");
838 XCTAssertNotNil(ckme, "Received a ckme");
840 XCTAssertEqual(ckme.wasCurrent, 10u, "Properly received wasCurrent");
846 -(void)testPCSCurrentPointerWriteFailure {
847 SecResetLocalSecuritydXPCFakeEntitlements();
848 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
849 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
850 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
852 NSNumber* servIdentifier = @3;
853 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
854 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
856 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
857 [self startCKKSSubsystem];
859 // Let things shake themselves out.
860 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"key state should enter 'ready'");
861 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
863 // Ensure there's no current pointer
864 [self fetchCurrentPointerExpectingError:false];
866 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
867 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
868 PCSServiceIdentifier:(NSNumber *)servIdentifier
869 PCSPublicKey:publicKey
870 PCSPublicIdentity:publicIdentity]];
872 NSDictionary* result = [self pcsAddItem:@"testaccount"
873 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
874 serviceIdentifier:(NSNumber*)servIdentifier
875 publicKey:(NSData*)publicKey
876 publicIdentity:(NSData*)publicIdentity
878 XCTAssertNotNil(result, "Received result from adding item");
880 // Check that the record is where we expect it in CloudKit
881 [self waitForCKModifications];
882 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
883 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
884 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
886 NSData* persistentRef = result[(id)kSecValuePersistentRef];
887 NSData* sha1 = result[(id)kSecAttrSHA1];
889 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
891 // Set the 'current' pointer.
892 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
894 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
895 (__bridge CFStringRef)@"pcsservice",
896 (__bridge CFStringRef)@"keychain",
897 (__bridge CFDataRef)persistentRef,
898 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
899 NSError* error = (__bridge NSError*)cferror;
900 XCTAssertNotNil(error, "Error setting current item when the write fails");
901 [setCurrentExpectation fulfill];
903 OCMVerifyAllWithDelay(self.mockDatabase, 40);
905 [self waitForExpectationsWithTimeout:8.0 handler:nil];
907 SecResetLocalSecuritydXPCFakeEntitlements();
910 - (void)testPCSCurrentRecoverFromDanglingPointer {
911 SecResetLocalSecuritydXPCFakeEntitlements();
912 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
913 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
914 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
916 NSNumber* servIdentifier = @3;
917 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
918 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
920 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
921 [self startCKKSSubsystem];
923 // Let things shake themselves out.
924 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"key state should enter 'ready'");
925 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
927 // Ensure there's no current pointer
928 [self fetchCurrentPointerExpectingError:false];
930 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
931 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
932 PCSServiceIdentifier:(NSNumber *)servIdentifier
933 PCSPublicKey:publicKey
934 PCSPublicIdentity:publicIdentity]];
936 NSDictionary* result = [self pcsAddItem:@"testaccount"
937 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
938 serviceIdentifier:(NSNumber*)servIdentifier
939 publicKey:(NSData*)publicKey
940 publicIdentity:(NSData*)publicIdentity
942 XCTAssertNotNil(result, "Received result from adding item");
944 // Check that the record is where we expect it in CloudKit
945 [self waitForCKModifications];
946 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
947 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
948 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
950 NSData* persistentRef = result[(id)kSecValuePersistentRef];
951 NSData* sha1 = result[(id)kSecAttrSHA1];
953 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
954 deletedRecordTypeCounts:nil
955 zoneID:self.keychainZoneID
956 checkModifiedRecord:nil
957 runAfterModification:nil];
959 // Set the 'current' pointer.
960 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
962 // Ensure that setting the current pointer sends a notification
963 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
964 (__bridge CFStringRef)@"pcsservice",
965 (__bridge CFStringRef)@"keychain",
966 (__bridge CFDataRef)persistentRef,
967 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
968 NSError* error = (__bridge NSError*)cferror;
969 XCTAssertNil(error, "No error setting current item");
970 [setCurrentExpectation fulfill];
972 OCMVerifyAllWithDelay(self.mockDatabase, 20);
973 [self waitForCKModifications];
975 [self waitForExpectationsWithTimeout:8.0 handler:nil];
977 // Delete the keychain item
978 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
979 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
980 (id)kSecClass : (id)kSecClassGenericPassword,
981 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
982 (id)kSecAttrAccount:@"testaccount",
983 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
984 }), "Should receive no error deleting item");
985 OCMVerifyAllWithDelay(self.mockDatabase, 20);
987 // Now, fetch the current pointer: we should get an error
988 [self fetchCurrentPointerExpectingError:false];
990 // Setting the current item pointer again, using a NULL old value, should work.
991 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
992 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
993 PCSServiceIdentifier:(NSNumber *)servIdentifier
994 PCSPublicKey:publicKey
995 PCSPublicIdentity:publicIdentity]];
997 result = [self pcsAddItem:@"testaccount2"
998 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
999 serviceIdentifier:(NSNumber*)servIdentifier
1000 publicKey:(NSData*)publicKey
1001 publicIdentity:(NSData*)publicIdentity
1002 expectingSync:true];
1003 XCTAssertNotNil(result, "Should have result from adding item2");
1005 persistentRef = result[(id)kSecValuePersistentRef];
1006 sha1 = result[(id)kSecAttrSHA1];
1008 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
1009 deletedRecordTypeCounts:nil
1010 zoneID:self.keychainZoneID
1011 checkModifiedRecord:nil
1012 runAfterModification:nil];
1014 // Set the 'current' pointer.
1015 setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
1017 // Ensure that setting the current pointer sends a notification
1018 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
1019 (__bridge CFStringRef)@"pcsservice",
1020 (__bridge CFStringRef)@"keychain",
1021 (__bridge CFDataRef)persistentRef,
1022 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
1023 NSError* error = (__bridge NSError*)cferror;
1024 XCTAssertNil(error, "No error setting current item");
1025 [setCurrentExpectation fulfill];
1027 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1028 [self waitForCKModifications];
1029 [self waitForExpectationsWithTimeout:8.0 handler:nil];
1031 SecResetLocalSecuritydXPCFakeEntitlements();
1034 -(void)testPCSCurrentSetConflictedItemAsCurrent {
1035 SecResetLocalSecuritydXPCFakeEntitlements();
1036 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
1037 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
1038 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
1040 NSNumber* servIdentifier = @3;
1041 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
1042 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
1044 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1045 [self startCKKSSubsystem];
1047 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1048 [self.keychainView waitUntilAllOperationsAreFinished];
1050 // Before CKKS can add the item, shove a conflicting one into CloudKit
1051 NSError* error = nil;
1053 NSString* account = @"testaccount";
1055 // Create an item in CloudKit that will conflict in both UUID and primary key
1056 NSData* itemdata = [[NSData alloc] initWithBase64EncodedString:@"YnBsaXN0MDDbAQIDBAUGBwgJCgsMDQ4PEBESEhMUFVZ2X0RhdGFUYWNjdFR0b21iVHN2Y2VUc2hhMVRtdXNyVGNkYXRUbWRhdFRwZG1uVGFncnBVY2xhc3NEYXNkZlt0ZXN0YWNjb3VudBAAUE8QFF7OzuEEGWTTwzzSp/rjY6ubHW2rQDNBv7zNQtQUQFJja18QF2NvbS5hcHBsZS5zZWN1cml0eS5ja2tzVGdlbnAIHyYrMDU6P0RJTlNZXmpsbYSFjpGrAAAAAAAAAQEAAAAAAAAAFgAAAAAAAAAAAAAAAAAAALA=" options:0];
1057 NSMutableDictionary * item = [[NSPropertyListSerialization propertyListWithData:itemdata
1060 error:&error] mutableCopy];
1061 XCTAssertNil(error, "Error should be nil parsing base64 item");
1063 item[@"v_Data"] = [@"conflictingdata" dataUsingEncoding:NSUTF8StringEncoding];
1064 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
1065 CKRecord* mismatchedRecord = [self newRecord:ckrid withNewItemData:item];
1066 [self.keychainZone addToZone: mismatchedRecord];
1068 self.keychainView.holdIncomingQueueOperation = [CKKSResultOperation named:@"hold-incoming" withBlock:^{
1069 secnotice("ckks", "Releasing process incoming queue hold");
1072 NSData* firstItemData = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding];
1074 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1075 NSDictionary* result = [self pcsAddItem:account
1077 serviceIdentifier:(NSNumber*)servIdentifier
1078 publicKey:(NSData*)publicKey
1079 publicIdentity:(NSData*)publicIdentity
1080 expectingSync:false];
1081 XCTAssertNotNil(result, "Should receive result from adding item");
1083 NSData* persistentRef = result[(id)kSecValuePersistentRef];
1084 NSData* sha1 = result[(id)kSecAttrSHA1];
1086 // Ensure that fetching the item without grabbing data returns the same SHA1
1087 NSDictionary* prefquery = @{(id)kSecClass : (id)kSecClassGenericPassword,
1088 (id)kSecReturnAttributes : @YES,
1089 (id)kSecAttrSynchronizable : @YES,
1090 (id)kSecAttrPersistentReference : persistentRef,
1091 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1093 CFTypeRef prefresult = NULL;
1094 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef)prefquery, &prefresult), "Should be able to find item by persistent ref");
1095 NSDictionary* newPersistentRefResult = (NSDictionary*) CFBridgingRelease(prefresult);
1097 XCTAssertNotNil(newPersistentRefResult, "Should have received item attributes");
1098 XCTAssertEqualObjects(newPersistentRefResult[(id)kSecAttrSHA1], sha1, "SHA1 should match between Add and Find (with data)");
1099 XCTAssertNil(newPersistentRefResult[(id)kSecValueData], "Should have returned no data");
1101 // Ensure that fetching the item and grabbing data returns the same SHA1
1102 prefquery = @{(id)kSecClass : (id)kSecClassGenericPassword,
1103 (id)kSecReturnAttributes : @YES,
1104 (id)kSecReturnData : @YES,
1105 (id)kSecAttrSynchronizable : @YES,
1106 (id)kSecAttrPersistentReference : persistentRef,
1107 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1109 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef)prefquery, &prefresult), "Should be able to find item by persistent ref");
1110 newPersistentRefResult = (NSDictionary*) CFBridgingRelease(prefresult);
1111 XCTAssertNotNil(newPersistentRefResult, "Should have received item attributes");
1112 XCTAssertEqualObjects(newPersistentRefResult[(id)kSecAttrSHA1], sha1, "SHA1 should match between Add and Find (with data)");
1113 XCTAssertEqualObjects(newPersistentRefResult[(id)kSecValueData], firstItemData, "Should have returned data matching the item we put in");
1115 // Set the current pointer to the result of adding this item. This should fail.
1116 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs before incoming queue operation"];
1117 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
1118 (__bridge CFStringRef)@"pcsservice",
1119 (__bridge CFStringRef)@"keychain",
1120 (__bridge CFDataRef)persistentRef,
1121 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
1122 XCTAssertNotNil((__bridge NSError*)cferror, "Should error setting current item to hash of item which failed to sync (before incoming queue operation)");
1123 [setCurrentExpectation fulfill];
1126 [self waitForExpectations:@[setCurrentExpectation] timeout:20];
1128 // Now, release the incoming queue processing and retry the failure
1129 [self.operationQueue addOperation:self.keychainView.holdIncomingQueueOperation];
1130 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
1132 setCurrentExpectation = [self expectationWithDescription: @"callback occurs after incoming queue operation"];
1133 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
1134 (__bridge CFStringRef)@"pcsservice",
1135 (__bridge CFStringRef)@"keychain",
1136 (__bridge CFDataRef)persistentRef,
1137 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
1138 XCTAssertNotNil((__bridge NSError*)cferror, "Should error setting current item to hash of item which failed to sync (after incoming queue operation)");
1139 [setCurrentExpectation fulfill];
1142 [self waitForExpectations:@[setCurrentExpectation] timeout:20];
1144 // Reissue a fetch and find the new persistent ref and sha1 for the item at this UUID
1145 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1147 // The conflicting item update should have won
1148 [self checkGenericPassword:@"conflictingdata" account:account];
1150 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
1151 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1152 (id)kSecAttrAccount : account,
1153 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1154 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1155 (id)kSecReturnAttributes: @YES,
1156 (id)kSecReturnPersistentRef: @YES,
1159 CFTypeRef cfresult = NULL;
1160 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Finding item %@", account);
1161 NSDictionary* newResult = CFBridgingRelease(cfresult);
1162 XCTAssertNotNil(newResult, "Received an item");
1164 NSData* newPersistentRef = newResult[(id)kSecValuePersistentRef];
1165 NSData* newSha1 = newResult[(id)kSecAttrSHA1];
1167 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
1168 deletedRecordTypeCounts:nil
1169 zoneID:self.keychainZoneID
1170 checkModifiedRecord:nil
1171 runAfterModification:nil];
1173 XCTestExpectation* newSetCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
1174 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
1175 (__bridge CFStringRef)@"pcsservice",
1176 (__bridge CFStringRef)@"keychain",
1177 (__bridge CFDataRef)newPersistentRef,
1178 (__bridge CFDataRef)newSha1, NULL, NULL, ^ (CFErrorRef cferror) {
1179 XCTAssertNil((__bridge NSError*)cferror, "Shouldn't error setting current item");
1180 [newSetCurrentExpectation fulfill];
1183 [self waitForExpectations:@[newSetCurrentExpectation] timeout:20];
1185 SecResetLocalSecuritydXPCFakeEntitlements();