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: @"50184A35-4480-E8BA-769B-567CF72F1EC0" 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: @"2DEA6136-2505-6BFD-E3E8-B44A6E39C3B5" 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 ckksnotice_global("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: @"50184A35-4480-E8BA-769B-567CF72F1EC0" 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: @"50184A35-4480-E8BA-769B-567CF72F1EC0" 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: @"10E76B80-CE1C-A52A-B0CB-462A2EBA05AF" 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:@"50184A35-4480-E8BA-769B-567CF72F1EC0"
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.injectedManager.zoneChangeFetcher 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.injectedManager.zoneChangeFetcher 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: @"50184A35-4480-E8BA-769B-567CF72F1EC0" 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:@"50184A35-4480-E8BA-769B-567CF72F1EC0"
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.injectedManager.zoneChangeFetcher 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];
670 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
671 [self.keychainView waitForFetchAndIncomingQueueProcessing];
672 [self waitForExpectations:@[keychainChanged] timeout:8];
674 [self fetchCurrentPointerExpectingError:false];
676 SecResetLocalSecuritydXPCFakeEntitlements();
680 - (void)testPCSCurrentPointerRecoverFromRecordExistsError {
681 SecResetLocalSecuritydXPCFakeEntitlements();
682 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
683 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
684 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
686 NSNumber* servIdentifier = @3;
687 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
688 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
690 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
691 [self startCKKSSubsystem];
693 // Let things shake themselves out.
694 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"key state should enter 'ready'");
695 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
697 // Ensure there's no current pointer
698 [self fetchCurrentPointerExpectingError:false];
700 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
701 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
702 PCSServiceIdentifier:(NSNumber *)servIdentifier
703 PCSPublicKey:publicKey
704 PCSPublicIdentity:publicIdentity]];
706 NSDictionary* result = [self pcsAddItem:@"testaccount"
707 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
708 serviceIdentifier:(NSNumber*)servIdentifier
709 publicKey:(NSData*)publicKey
710 publicIdentity:(NSData*)publicIdentity
712 XCTAssertNotNil(result, "Received result from adding item");
714 // Check that the record is where we expect it in CloudKit
715 [self waitForCKModifications];
716 NSString* recordUUID = @"50184A35-4480-E8BA-769B-567CF72F1EC0";
717 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName:recordUUID zoneID:self.keychainZoneID];
718 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
719 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
721 // Someone else sets the current record pointer
722 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
723 currentItemUUID:recordUUID
724 state:SecCKKSProcessedStateRemote
725 zoneID:self.keychainZoneID
726 encodedCKRecord:nil];
727 XCTAssertNotNil(cip, "Should have created a CIP");
728 CKRecord* cipRecord = [cip CKRecordWithZoneID:self.keychainZoneID];
729 XCTAssertNotNil(cipRecord, "Should have created a CKRecord for this CIP");
730 [self.keychainZone addToZone: cipRecord];
732 NSData* persistentRef = result[(id)kSecValuePersistentRef];
733 NSData* sha1 = result[(id)kSecAttrSHA1];
735 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
737 // Set the 'current' pointer.
738 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
740 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
741 (__bridge CFStringRef)@"pcsservice",
742 (__bridge CFStringRef)@"keychain",
743 (__bridge CFDataRef)persistentRef,
744 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
745 NSError* error = (__bridge NSError*)cferror;
746 XCTAssertNotNil(error, "Should have received an error setting current item (because of conflict)");
747 [setCurrentExpectation fulfill];
749 OCMVerifyAllWithDelay(self.mockDatabase, 20);
750 [self waitForCKModifications];
751 [self waitForExpectationsWithTimeout:8.0 handler:nil];
753 [self.keychainView waitUntilAllOperationsAreFinished];
755 [self fetchCurrentPointer:false persistentRef:persistentRef];
757 SecResetLocalSecuritydXPCFakeEntitlements();
760 - (void)testPCSCurrentPointerWasCurrent {
761 SecResetLocalSecuritydXPCFakeEntitlements();
762 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
763 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
764 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
766 NSNumber* servIdentifier = @3;
767 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
768 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
770 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
771 [self startCKKSSubsystem];
773 // Let things shake themselves out.
774 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"key state should enter 'ready'");
775 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
777 // Ensure there's no current pointer
778 [self fetchCurrentPointerExpectingError:false];
780 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
781 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
782 PCSServiceIdentifier:(NSNumber *)servIdentifier
783 PCSPublicKey:publicKey
784 PCSPublicIdentity:publicIdentity]];
786 NSDictionary* result = [self pcsAddItem:@"testaccount"
787 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
788 serviceIdentifier:(NSNumber*)servIdentifier
789 publicKey:(NSData*)publicKey
790 publicIdentity:(NSData*)publicIdentity
792 XCTAssertNotNil(result, "Received result from adding item");
794 // Check that the record is where we expect it in CloudKit
795 [self waitForCKModifications];
796 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
797 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
798 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
800 NSData* persistentRef = result[(id)kSecValuePersistentRef];
801 NSData* sha1 = result[(id)kSecAttrSHA1];
803 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
804 deletedRecordTypeCounts:nil
805 zoneID:self.keychainZoneID
806 checkModifiedRecord:nil
807 runAfterModification:nil];
809 // Set the 'current' pointer.
810 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
812 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
813 (__bridge CFStringRef)@"pcsservice",
814 (__bridge CFStringRef)@"keychain",
815 (__bridge CFDataRef)persistentRef,
816 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
817 NSError* error = (__bridge NSError*)cferror;
818 XCTAssertNil(error, "No error setting current item");
819 [setCurrentExpectation fulfill];
821 OCMVerifyAllWithDelay(self.mockDatabase, 20);
822 [self waitForExpectationsWithTimeout:8.0 handler:nil];
823 [self waitForCKModifications];
825 // Set the 'was current' flag on the record
826 CKRecord* modifiedRecord = [record copy];
827 modifiedRecord[SecCKRecordServerWasCurrent] = [NSNumber numberWithInteger:10];
828 [self.keychainZone addToZone:modifiedRecord];
830 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
831 [self.keychainView waitForFetchAndIncomingQueueProcessing];
833 // Check that the number is on the CKKSMirrorEntry
834 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
835 NSError* error = nil;
836 CKKSMirrorEntry* ckme = [CKKSMirrorEntry fromDatabase:@"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID error:&error];
838 XCTAssertNil(error, "no error fetching ckme");
839 XCTAssertNotNil(ckme, "Received a ckme");
841 XCTAssertEqual(ckme.wasCurrent, 10u, "Properly received wasCurrent");
845 -(void)testPCSCurrentPointerWriteFailure {
846 SecResetLocalSecuritydXPCFakeEntitlements();
847 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
848 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
849 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
851 NSNumber* servIdentifier = @3;
852 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
853 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
855 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
856 [self startCKKSSubsystem];
858 // Let things shake themselves out.
859 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"key state should enter 'ready'");
860 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
862 // Ensure there's no current pointer
863 [self fetchCurrentPointerExpectingError:false];
865 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
866 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
867 PCSServiceIdentifier:(NSNumber *)servIdentifier
868 PCSPublicKey:publicKey
869 PCSPublicIdentity:publicIdentity]];
871 NSDictionary* result = [self pcsAddItem:@"testaccount"
872 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
873 serviceIdentifier:(NSNumber*)servIdentifier
874 publicKey:(NSData*)publicKey
875 publicIdentity:(NSData*)publicIdentity
877 XCTAssertNotNil(result, "Received result from adding item");
879 // Check that the record is where we expect it in CloudKit
880 [self waitForCKModifications];
881 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
882 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
883 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
885 NSData* persistentRef = result[(id)kSecValuePersistentRef];
886 NSData* sha1 = result[(id)kSecAttrSHA1];
888 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
890 // Set the 'current' pointer.
891 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
893 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
894 (__bridge CFStringRef)@"pcsservice",
895 (__bridge CFStringRef)@"keychain",
896 (__bridge CFDataRef)persistentRef,
897 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
898 NSError* error = (__bridge NSError*)cferror;
899 XCTAssertNotNil(error, "Error setting current item when the write fails");
900 [setCurrentExpectation fulfill];
902 OCMVerifyAllWithDelay(self.mockDatabase, 40);
904 [self waitForExpectationsWithTimeout:8.0 handler:nil];
906 SecResetLocalSecuritydXPCFakeEntitlements();
909 - (void)testPCSCurrentRecoverFromDanglingPointer {
910 SecResetLocalSecuritydXPCFakeEntitlements();
911 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
912 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
913 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
915 NSNumber* servIdentifier = @3;
916 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
917 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
919 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
920 [self startCKKSSubsystem];
922 // Let things shake themselves out.
923 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"key state should enter 'ready'");
924 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
926 // Ensure there's no current pointer
927 [self fetchCurrentPointerExpectingError:false];
929 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
930 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
931 PCSServiceIdentifier:(NSNumber *)servIdentifier
932 PCSPublicKey:publicKey
933 PCSPublicIdentity:publicIdentity]];
935 NSDictionary* result = [self pcsAddItem:@"testaccount"
936 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
937 serviceIdentifier:(NSNumber*)servIdentifier
938 publicKey:(NSData*)publicKey
939 publicIdentity:(NSData*)publicIdentity
941 XCTAssertNotNil(result, "Received result from adding item");
943 // Check that the record is where we expect it in CloudKit
944 [self waitForCKModifications];
945 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
946 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
947 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
949 NSData* persistentRef = result[(id)kSecValuePersistentRef];
950 NSData* sha1 = result[(id)kSecAttrSHA1];
952 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
953 deletedRecordTypeCounts:nil
954 zoneID:self.keychainZoneID
955 checkModifiedRecord:nil
956 runAfterModification:nil];
958 // Set the 'current' pointer.
959 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
961 // Ensure that setting the current pointer sends a notification
962 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
963 (__bridge CFStringRef)@"pcsservice",
964 (__bridge CFStringRef)@"keychain",
965 (__bridge CFDataRef)persistentRef,
966 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
967 NSError* error = (__bridge NSError*)cferror;
968 XCTAssertNil(error, "No error setting current item");
969 [setCurrentExpectation fulfill];
971 OCMVerifyAllWithDelay(self.mockDatabase, 20);
972 [self waitForCKModifications];
974 [self waitForExpectationsWithTimeout:8.0 handler:nil];
976 // Delete the keychain item
977 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
978 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
979 (id)kSecClass : (id)kSecClassGenericPassword,
980 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
981 (id)kSecAttrAccount:@"testaccount",
982 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
983 }), "Should receive no error deleting item");
984 OCMVerifyAllWithDelay(self.mockDatabase, 20);
986 // Now, fetch the current pointer: we should get an error
987 [self fetchCurrentPointerExpectingError:false];
989 // Setting the current item pointer again, using a NULL old value, should work.
990 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
991 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
992 PCSServiceIdentifier:(NSNumber *)servIdentifier
993 PCSPublicKey:publicKey
994 PCSPublicIdentity:publicIdentity]];
996 result = [self pcsAddItem:@"testaccount2"
997 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
998 serviceIdentifier:(NSNumber*)servIdentifier
999 publicKey:(NSData*)publicKey
1000 publicIdentity:(NSData*)publicIdentity
1001 expectingSync:true];
1002 XCTAssertNotNil(result, "Should have result from adding item2");
1004 persistentRef = result[(id)kSecValuePersistentRef];
1005 sha1 = result[(id)kSecAttrSHA1];
1007 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
1008 deletedRecordTypeCounts:nil
1009 zoneID:self.keychainZoneID
1010 checkModifiedRecord:nil
1011 runAfterModification:nil];
1013 // Set the 'current' pointer.
1014 setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
1016 // Ensure that setting the current pointer sends a notification
1017 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
1018 (__bridge CFStringRef)@"pcsservice",
1019 (__bridge CFStringRef)@"keychain",
1020 (__bridge CFDataRef)persistentRef,
1021 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
1022 NSError* error = (__bridge NSError*)cferror;
1023 XCTAssertNil(error, "No error setting current item");
1024 [setCurrentExpectation fulfill];
1026 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1027 [self waitForCKModifications];
1028 [self waitForExpectationsWithTimeout:8.0 handler:nil];
1030 SecResetLocalSecuritydXPCFakeEntitlements();
1033 -(void)testPCSCurrentSetConflictedItemAsCurrent {
1034 SecResetLocalSecuritydXPCFakeEntitlements();
1035 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
1036 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
1037 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
1039 NSNumber* servIdentifier = @3;
1040 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
1041 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
1043 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1044 [self startCKKSSubsystem];
1046 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1047 [self.keychainView waitUntilAllOperationsAreFinished];
1049 // Before CKKS can add the item, shove a conflicting one into CloudKit
1050 NSError* error = nil;
1052 NSString* account = @"testaccount";
1054 // Create an item in CloudKit that will conflict in both UUID and primary key
1055 NSData* itemdata = [[NSData alloc] initWithBase64EncodedString:@"YnBsaXN0MDDbAQIDBAUGBwgJCgsMDQ4PEBESEhMUFVZ2X0RhdGFUYWNjdFR0b21iVHN2Y2VUc2hhMVRtdXNyVGNkYXRUbWRhdFRwZG1uVGFncnBVY2xhc3NEYXNkZlt0ZXN0YWNjb3VudBAAUE8QFF7OzuEEGWTTwzzSp/rjY6ubHW2rQDNBv7zNQtQUQFJja18QF2NvbS5hcHBsZS5zZWN1cml0eS5ja2tzVGdlbnAIHyYrMDU6P0RJTlNZXmpsbYSFjpGrAAAAAAAAAQEAAAAAAAAAFgAAAAAAAAAAAAAAAAAAALA=" options:0];
1056 NSMutableDictionary * item = [[NSPropertyListSerialization propertyListWithData:itemdata
1059 error:&error] mutableCopy];
1060 XCTAssertNil(error, "Error should be nil parsing base64 item");
1062 item[@"v_Data"] = [@"conflictingdata" dataUsingEncoding:NSUTF8StringEncoding];
1063 item[@"vwht"] = @"keychain";
1064 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"50184A35-4480-E8BA-769B-567CF72F1EC0" 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 ckksnotice_global("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.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
1146 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1148 // The conflicting item update should have won
1149 [self checkGenericPassword:@"conflictingdata" account:account];
1151 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
1152 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1153 (id)kSecAttrAccount : account,
1154 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1155 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1156 (id)kSecReturnAttributes: @YES,
1157 (id)kSecReturnPersistentRef: @YES,
1160 CFTypeRef cfresult = NULL;
1161 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Finding item %@", account);
1162 NSDictionary* newResult = CFBridgingRelease(cfresult);
1163 XCTAssertNotNil(newResult, "Received an item");
1165 NSData* newPersistentRef = newResult[(id)kSecValuePersistentRef];
1166 NSData* newSha1 = newResult[(id)kSecAttrSHA1];
1168 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
1169 deletedRecordTypeCounts:nil
1170 zoneID:self.keychainZoneID
1171 checkModifiedRecord:nil
1172 runAfterModification:nil];
1174 XCTestExpectation* newSetCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
1175 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
1176 (__bridge CFStringRef)@"pcsservice",
1177 (__bridge CFStringRef)@"keychain",
1178 (__bridge CFDataRef)newPersistentRef,
1179 (__bridge CFDataRef)newSha1, NULL, NULL, ^ (CFErrorRef cferror) {
1180 XCTAssertNil((__bridge NSError*)cferror, "Shouldn't error setting current item");
1181 [newSetCurrentExpectation fulfill];
1184 [self waitForExpectations:@[newSetCurrentExpectation] timeout:20];
1186 SecResetLocalSecuritydXPCFakeEntitlements();