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"
42 #import "keychain/ckks/tests/CKKSTests.h"
43 #import "keychain/ckks/tests/CKKSTests+API.h"
45 @implementation CloudKitKeychainSyncingTests (CurrentPointerAPITests)
47 -(void)fetchCurrentPointer:(bool)cached persistentRef:(NSData*)persistentRef
49 XCTestExpectation* currentExpectation = [self expectationWithDescription: @"callback occurs"];
50 SecItemFetchCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
51 (__bridge CFStringRef)@"pcsservice",
52 (__bridge CFStringRef)@"keychain",
54 ^(CFDataRef currentPersistentRef, CFErrorRef cferror) {
55 XCTAssertNotNil((__bridge id)currentPersistentRef, "current item exists");
56 XCTAssertNil((__bridge id)cferror, "no error exists when there's a current item");
57 XCTAssertEqualObjects(persistentRef, (__bridge id)currentPersistentRef, "persistent ref matches expected persistent ref");
58 [currentExpectation fulfill];
60 [self waitForExpectationsWithTimeout:8.0 handler:nil];
62 -(void)fetchCurrentPointerExpectingError:(bool)cached
64 XCTestExpectation* currentExpectation = [self expectationWithDescription: @"callback occurs"];
65 SecItemFetchCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
66 (__bridge CFStringRef)@"pcsservice",
67 (__bridge CFStringRef)@"keychain",
69 ^(CFDataRef currentPersistentRef, CFErrorRef cferror) {
70 XCTAssertNil((__bridge id)currentPersistentRef, "no current item exists");
71 XCTAssertNotNil((__bridge id)cferror, "Error exists when there's a current item");
72 [currentExpectation fulfill];
74 [self waitForExpectationsWithTimeout:8.0 handler:nil];
77 - (void)testPCSFetchCurrentPointerCachedAndUncached {
78 SecResetLocalSecuritydXPCFakeEntitlements();
79 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
80 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
81 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
83 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
84 [self startCKKSSubsystem];
86 [self.keychainView waitForKeyHierarchyReadiness];
88 // Ensure that local queries don't hit the server.
89 self.silentFetchesAllowed = false;
90 [self fetchCurrentPointerExpectingError:false];
92 // And ensure that global queries do.
94 [self fetchCurrentPointerExpectingError:true];
95 OCMVerifyAllWithDelay(self.mockDatabase, 8);
96 SecResetLocalSecuritydXPCFakeEntitlements();
99 - (void)testPCSCurrentPointerAddAndUpdate {
100 SecResetLocalSecuritydXPCFakeEntitlements();
101 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
102 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
103 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
105 NSNumber* servIdentifier = @3;
106 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
107 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
109 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
110 [self startCKKSSubsystem];
112 // Let things shake themselves out.
113 [self.keychainView waitForKeyHierarchyReadiness];
115 // Ensure there's no current pointer
116 [self fetchCurrentPointerExpectingError:false];
118 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
120 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
121 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
122 PCSServiceIdentifier:(NSNumber *)servIdentifier
123 PCSPublicKey:publicKey
124 PCSPublicIdentity:publicIdentity]];
126 NSDictionary* result = [self pcsAddItem:@"testaccount"
127 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
128 serviceIdentifier:(NSNumber*)servIdentifier
129 publicKey:(NSData*)publicKey
130 publicIdentity:(NSData*)publicIdentity
132 XCTAssertNotNil(result, "Received result from adding item");
133 [self waitForExpectations:@[keychainChanged] timeout:1];
135 // Check that the record is where we expect it in CloudKit
136 [self waitForCKModifications];
137 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
138 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
139 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
141 NSData* persistentRef = result[(id)kSecValuePersistentRef];
142 NSData* sha1 = result[(id)kSecAttrSHA1];
144 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
145 deletedRecordTypeCounts:nil
146 zoneID:self.keychainZoneID
147 checkModifiedRecord:nil
148 runAfterModification:nil];
150 // Set the 'current' pointer.
151 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
153 // Ensure that setting the current pointer sends a notification
154 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
156 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
157 (__bridge CFStringRef)@"pcsservice",
158 (__bridge CFStringRef)@"keychain",
159 (__bridge CFDataRef)persistentRef,
160 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
161 NSError* error = (__bridge NSError*)cferror;
162 XCTAssertNil(error, "No error setting current item");
163 [setCurrentExpectation fulfill];
165 OCMVerifyAllWithDelay(self.mockDatabase, 8);
166 [self waitForExpectations:@[keychainChanged] timeout:1];
167 [self waitForCKModifications];
169 [self waitForExpectationsWithTimeout:8.0 handler:nil];
171 CKRecord* currentItemPointer = self.keychainZone.currentDatabase[[[CKRecordID alloc] initWithRecordName:@"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID]];
172 XCTAssertNotNil(currentItemPointer, "Found a CKRecord at the expected location in CloudKit");
173 XCTAssertEqualObjects(currentItemPointer.recordType, SecCKRecordCurrentItemType, "Saved CKRecord is correct type");
174 XCTAssertEqualObjects(((CKReference*)currentItemPointer[SecCKRecordItemRefKey]).recordID, pcsItemRecordID, "Current Item record points to correct record");
176 // Check that the status APIs return the right value
177 [self fetchCurrentPointer:false persistentRef:persistentRef];
179 // Rad. If we got here, adding a new current item pointer works. Let's see if we can modify one.
180 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
182 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
183 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
184 PCSServiceIdentifier:(NSNumber *)servIdentifier
185 PCSPublicKey:publicKey
186 PCSPublicIdentity:publicIdentity]];
188 result = [self pcsAddItem:@"tOTHER-ITEM"
189 data:[@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding]
190 serviceIdentifier:(NSNumber*)servIdentifier
191 publicKey:(NSData*)publicKey
192 publicIdentity:(NSData*)publicIdentity
194 XCTAssertNotNil(result, "Received result from adding item");
195 [self waitForExpectations:@[keychainChanged] timeout:1];
197 // Check that the record is where we expect it
198 [self waitForCKModifications];
199 CKRecordID* pcsOtherItemRecordID = [[CKRecordID alloc] initWithRecordName: @"878BEAA6-1EE9-1079-1025-E6832AC8F2F3" zoneID:self.keychainZoneID];
200 CKRecord* recordOther = self.keychainZone.currentDatabase[pcsOtherItemRecordID];
201 XCTAssertNotNil(recordOther, "Found other record in CloudKit at expected UUID");
203 NSData* otherPersistentRef = result[(id)kSecValuePersistentRef];
204 NSData* otherSha1 = result[(id)kSecAttrSHA1];
206 // change the 'current' pointer.
208 // Refetch the old item's hash, just in case it's changed (it does, about 50% of the time. I'm not sure why).
209 CFTypeRef cfresult = NULL;
210 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) @{
211 (id)kSecValuePersistentRef : persistentRef,
212 (id)kSecReturnAttributes : @YES,
213 }, &cfresult), "Found original item by persistent reference");
215 XCTAssertNotNil((__bridge id)cfresult, "Received an item by finding persistent reference");
216 NSData* actualSHA1 = CFBridgingRelease(CFRetainSafe(CFDictionaryGetValue(cfresult, kSecAttrSHA1)));
217 XCTAssertNotNil(actualSHA1, "Have a SHA1 for the original item");
218 CFReleaseNull(cfresult);
220 if(![actualSHA1 isEqual:sha1]) {
221 secnotice("ckks", "SHA1s don't match, but why?");
224 XCTestExpectation* otherSetCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
226 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
227 deletedRecordTypeCounts:nil
228 zoneID:self.keychainZoneID
229 checkModifiedRecord:nil
230 runAfterModification:nil];
232 // Ensure that setting the current pointer sends a notification
233 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
235 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
236 (__bridge CFStringRef)@"pcsservice",
237 (__bridge CFStringRef)@"keychain",
238 (__bridge CFDataRef)otherPersistentRef,
239 (__bridge CFDataRef)otherSha1,
240 (__bridge CFDataRef)persistentRef,
241 (__bridge CFDataRef)actualSHA1, ^ (CFErrorRef cferror) {
242 NSError* error = (__bridge NSError*)cferror;
243 XCTAssertNil(error, "No error setting current item");
244 [otherSetCurrentExpectation fulfill];
246 OCMVerifyAllWithDelay(self.mockDatabase, 8);
247 [self waitForExpectations:@[keychainChanged] timeout:1];
248 [self waitForCKModifications];
250 [self waitForExpectationsWithTimeout:8.0 handler:nil];
252 currentItemPointer = self.keychainZone.currentDatabase[[[CKRecordID alloc] initWithRecordName:@"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID]];
253 XCTAssertNotNil(currentItemPointer, "Found a CKRecord at the expected location in CloudKit");
254 XCTAssertEqualObjects(currentItemPointer.recordType, SecCKRecordCurrentItemType, "Saved CKRecord is correct type");
255 XCTAssertEqualObjects(((CKReference*)currentItemPointer[SecCKRecordItemRefKey]).recordID, pcsOtherItemRecordID, "Current Item record points to updated record");
258 [self fetchCurrentPointer:false persistentRef:otherPersistentRef];
259 [self fetchCurrentPointer:true persistentRef:otherPersistentRef];
261 SecResetLocalSecuritydXPCFakeEntitlements();
264 - (void)testPCSCurrentPointerAddNoCloudKitAccount {
265 SecResetLocalSecuritydXPCFakeEntitlements();
266 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
267 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
268 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
270 NSNumber* servIdentifier = @3;
271 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
272 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
274 // Entirely signed out of iCloud. all current record writes should fail.
275 self.accountStatus = CKAccountStatusNoAccount;
276 self.circleStatus = kSOSCCNotInCircle;
277 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
279 self.silentFetchesAllowed = false;
280 [self startCKKSSubsystem];
282 // Ensure there's no current pointer
283 [self fetchCurrentPointerExpectingError:false];
285 // Should NOT add an item to CloudKit
286 NSDictionary* result = [self pcsAddItem:@"testaccount"
287 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
288 serviceIdentifier:(NSNumber*)servIdentifier
289 publicKey:(NSData*)publicKey
290 publicIdentity:(NSData*)publicIdentity
291 expectingSync:false];
292 XCTAssertNotNil(result, "Received result from adding item");
294 NSData* persistentRef = result[(id)kSecValuePersistentRef];
295 NSData* sha1 = result[(id)kSecAttrSHA1];
297 // Set the 'current' pointer. This should fail.
298 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
300 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
301 (__bridge CFStringRef)@"pcsservice",
302 (__bridge CFStringRef)@"keychain",
303 (__bridge CFDataRef)persistentRef,
304 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
305 NSError* error = (__bridge NSError*)cferror;
306 XCTAssertNotNil(error, "Error setting current item with no CloudKit account");
307 [setCurrentExpectation fulfill];
310 [self waitForExpectationsWithTimeout:8.0 handler:nil];
311 SecResetLocalSecuritydXPCFakeEntitlements();
314 - (void)testPCSCurrentPointerAddNonSyncItem {
315 SecResetLocalSecuritydXPCFakeEntitlements();
316 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
317 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
318 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
320 NSNumber* servIdentifier = @3;
321 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
322 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
324 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
325 [self startCKKSSubsystem];
326 [self.keychainView waitForKeyHierarchyReadiness];
328 NSMutableDictionary* query = [self pcsAddItemQuery:@"account"
329 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
330 serviceIdentifier:servIdentifier
332 publicIdentity:publicIdentity];
333 query[(id)kSecAttrSynchronizable] = @NO;
335 CFTypeRef cfresult = NULL;
336 XCTestExpectation* syncExpectation = [self expectationWithDescription: @"_SecItemAddAndNotifyOnSync callback occured"];
338 // Note that you will NOT receive a notification here, since you're adding a nonsync item
339 syncExpectation.inverted = YES;
341 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, &cfresult, ^(bool didSync, CFErrorRef error) {
342 XCTAssertFalse(didSync, "Item did not sync");
343 XCTAssertNotNil((__bridge NSError*)error, "Error syncing item");
345 [syncExpectation fulfill];
346 }), @"_SecItemAddAndNotifyOnSync succeeded");
348 // We don't expect this callback to fire, so give it a second or so
349 [self waitForExpectations:@[syncExpectation] timeout:2.0];
351 NSDictionary* result = CFBridgingRelease(cfresult);
353 XCTAssertNotNil(result, "Received result from adding item");
355 NSData* persistentRef = result[(id)kSecValuePersistentRef];
356 NSData* sha1 = result[(id)kSecAttrSHA1];
358 // Set the 'current' pointer. This should fail.
359 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
361 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
362 (__bridge CFStringRef)@"pcsservice",
363 (__bridge CFStringRef)@"keychain",
364 (__bridge CFDataRef)persistentRef,
365 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
366 NSError* error = (__bridge NSError*)cferror;
367 XCTAssertNotNil(error, "Error setting current item to nonsyncable item");
368 [setCurrentExpectation fulfill];
371 [self waitForExpectations:@[setCurrentExpectation] timeout:8.0];
372 SecResetLocalSecuritydXPCFakeEntitlements();
375 - (void)testPCSCurrentPointerReceive {
376 SecResetLocalSecuritydXPCFakeEntitlements();
377 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
378 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
379 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
381 NSNumber* servIdentifier = @3;
382 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
383 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
385 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
386 [self startCKKSSubsystem];
388 // Let things shake themselves out.
389 [self.keychainView waitForKeyHierarchyReadiness];
391 // Ensure there's no current pointer
392 [self fetchCurrentPointerExpectingError:false];
394 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
395 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
396 PCSServiceIdentifier:(NSNumber *)servIdentifier
397 PCSPublicKey:publicKey
398 PCSPublicIdentity:publicIdentity]];
400 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
402 NSDictionary* result = [self pcsAddItem:@"testaccount"
403 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
404 serviceIdentifier:(NSNumber*)servIdentifier
405 publicKey:(NSData*)publicKey
406 publicIdentity:(NSData*)publicIdentity
408 XCTAssertNotNil(result, "Received result from adding item");
409 NSData* persistentRef = result[(id)kSecValuePersistentRef];
411 [self waitForExpectations:@[keychainChanged] timeout:1];
413 // Check that the record is where we expect it in CloudKit
414 [self waitForCKModifications];
415 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
416 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
417 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
419 // Still no current pointer.
420 [self fetchCurrentPointerExpectingError:false];
422 // Another machine comes along and updates the pointer!
423 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
424 currentItemUUID:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3"
425 state:SecCKKSProcessedStateRemote
426 zoneID:self.keychainZoneID
427 encodedCKRecord:nil];
428 [self.keychainZone addToZone: [cip CKRecordWithZoneID:self.keychainZoneID]];
429 CKRecordID* currentPointerRecordID = [[CKRecordID alloc] initWithRecordName: @"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID];
430 CKRecord* currentPointerRecord = self.keychainZone.currentDatabase[currentPointerRecordID];
431 XCTAssertNotNil(currentPointerRecord, "Found record in CloudKit at expected UUID");
433 // Ensure that receiving the current item pointer generates a notification
434 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
436 [self.keychainView notifyZoneChange:nil];
437 [self.keychainView waitForFetchAndIncomingQueueProcessing];
439 [self waitForExpectations:@[keychainChanged] timeout:1];
441 [self fetchCurrentPointer:false persistentRef:persistentRef];
443 SecResetLocalSecuritydXPCFakeEntitlements();
447 - (void)testPCSCurrentPointerReceiveDelete {
448 SecResetLocalSecuritydXPCFakeEntitlements();
449 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
450 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
451 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
453 NSNumber* servIdentifier = @3;
454 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
455 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
457 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
458 [self startCKKSSubsystem];
460 // Let things shake themselves out.
461 [self.keychainView waitForKeyHierarchyReadiness];
463 // Ensure there's no current pointer
464 [self fetchCurrentPointerExpectingError:false];
466 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
467 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
468 PCSServiceIdentifier:(NSNumber *)servIdentifier
469 PCSPublicKey:publicKey
470 PCSPublicIdentity:publicIdentity]];
472 NSDictionary* result = [self pcsAddItem:@"testaccount"
473 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
474 serviceIdentifier:(NSNumber*)servIdentifier
475 publicKey:(NSData*)publicKey
476 publicIdentity:(NSData*)publicIdentity
478 XCTAssertNotNil(result, "Received result from adding item");
479 NSData* persistentRef = result[(id)kSecValuePersistentRef];
481 // Check that the record is where we expect it in CloudKit
482 [self waitForCKModifications];
483 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
484 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
485 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
487 // Still no current pointer.
488 [self fetchCurrentPointerExpectingError:false];
490 // Another machine comes along and updates the pointer!
491 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
492 currentItemUUID:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3"
493 state:SecCKKSProcessedStateRemote
494 zoneID:self.keychainZoneID
495 encodedCKRecord:nil];
496 [self.keychainZone addToZone: [cip CKRecordWithZoneID:self.keychainZoneID]];
497 CKRecordID* currentPointerRecordID = [[CKRecordID alloc] initWithRecordName: @"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID];
498 CKRecord* currentPointerRecord = self.keychainZone.currentDatabase[currentPointerRecordID];
499 XCTAssertNotNil(currentPointerRecord, "Found record in CloudKit at expected UUID");
501 [self.keychainView notifyZoneChange:nil];
502 [self.keychainView waitForFetchAndIncomingQueueProcessing];
504 [self fetchCurrentPointer:false persistentRef:persistentRef];
506 // Ensure that receiving the current item pointer generates a notification
507 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
509 // Another machine comes along and deletes the pointer!
510 [self.keychainZone deleteCKRecordIDFromZone: currentPointerRecordID];
511 [self.keychainView notifyZoneChange:nil];
512 [self.keychainView waitForFetchAndIncomingQueueProcessing];
513 [self waitForExpectations:@[keychainChanged] timeout:1];
515 [self fetchCurrentPointerExpectingError:false];
517 SecResetLocalSecuritydXPCFakeEntitlements();
521 - (void)testPCSCurrentPointerRecoverFromRecordExistsError {
522 SecResetLocalSecuritydXPCFakeEntitlements();
523 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
524 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
525 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
527 NSNumber* servIdentifier = @3;
528 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
529 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
531 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
532 [self startCKKSSubsystem];
534 // Let things shake themselves out.
535 [self.keychainView waitForKeyHierarchyReadiness];
537 // Ensure there's no current pointer
538 [self fetchCurrentPointerExpectingError:false];
540 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
541 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
542 PCSServiceIdentifier:(NSNumber *)servIdentifier
543 PCSPublicKey:publicKey
544 PCSPublicIdentity:publicIdentity]];
546 NSDictionary* result = [self pcsAddItem:@"testaccount"
547 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
548 serviceIdentifier:(NSNumber*)servIdentifier
549 publicKey:(NSData*)publicKey
550 publicIdentity:(NSData*)publicIdentity
552 XCTAssertNotNil(result, "Received result from adding item");
554 // Check that the record is where we expect it in CloudKit
555 [self waitForCKModifications];
556 NSString* recordUUID = @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3";
557 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName:recordUUID zoneID:self.keychainZoneID];
558 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
559 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
561 // Someone else sets the current record pointer
562 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
563 currentItemUUID:recordUUID
564 state:SecCKKSProcessedStateRemote
565 zoneID:self.keychainZoneID
566 encodedCKRecord:nil];
567 XCTAssertNotNil(cip, "Should have created a CIP");
568 CKRecord* cipRecord = [cip CKRecordWithZoneID:self.keychainZoneID];
569 XCTAssertNotNil(cipRecord, "Should have created a CKRecord for this CIP");
570 [self.keychainZone addToZone: cipRecord];
572 NSData* persistentRef = result[(id)kSecValuePersistentRef];
573 NSData* sha1 = result[(id)kSecAttrSHA1];
575 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
577 // Set the 'current' pointer.
578 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
580 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
581 (__bridge CFStringRef)@"pcsservice",
582 (__bridge CFStringRef)@"keychain",
583 (__bridge CFDataRef)persistentRef,
584 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
585 NSError* error = (__bridge NSError*)cferror;
586 XCTAssertNotNil(error, "Should have received an error setting current item (because of conflict)");
587 [setCurrentExpectation fulfill];
589 OCMVerifyAllWithDelay(self.mockDatabase, 8);
590 [self waitForCKModifications];
591 [self waitForExpectationsWithTimeout:8.0 handler:nil];
593 [self.keychainView waitUntilAllOperationsAreFinished];
595 [self fetchCurrentPointer:false persistentRef:persistentRef];
597 SecResetLocalSecuritydXPCFakeEntitlements();
600 - (void)testPCSCurrentPointerWasCurrent {
601 SecResetLocalSecuritydXPCFakeEntitlements();
602 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
603 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
604 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
606 NSNumber* servIdentifier = @3;
607 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
608 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
610 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
611 [self startCKKSSubsystem];
613 // Let things shake themselves out.
614 [self.keychainView waitForKeyHierarchyReadiness];
616 // Ensure there's no current pointer
617 [self fetchCurrentPointerExpectingError:false];
619 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
620 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
621 PCSServiceIdentifier:(NSNumber *)servIdentifier
622 PCSPublicKey:publicKey
623 PCSPublicIdentity:publicIdentity]];
625 NSDictionary* result = [self pcsAddItem:@"testaccount"
626 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
627 serviceIdentifier:(NSNumber*)servIdentifier
628 publicKey:(NSData*)publicKey
629 publicIdentity:(NSData*)publicIdentity
631 XCTAssertNotNil(result, "Received result from adding item");
633 // Check that the record is where we expect it in CloudKit
634 [self waitForCKModifications];
635 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
636 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
637 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
639 NSData* persistentRef = result[(id)kSecValuePersistentRef];
640 NSData* sha1 = result[(id)kSecAttrSHA1];
642 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
643 deletedRecordTypeCounts:nil
644 zoneID:self.keychainZoneID
645 checkModifiedRecord:nil
646 runAfterModification:nil];
648 // Set the 'current' pointer.
649 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
651 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
652 (__bridge CFStringRef)@"pcsservice",
653 (__bridge CFStringRef)@"keychain",
654 (__bridge CFDataRef)persistentRef,
655 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
656 NSError* error = (__bridge NSError*)cferror;
657 XCTAssertNil(error, "No error setting current item");
658 [setCurrentExpectation fulfill];
660 OCMVerifyAllWithDelay(self.mockDatabase, 8);
661 [self waitForExpectationsWithTimeout:8.0 handler:nil];
662 [self waitForCKModifications];
664 // Set the 'was current' flag on the record
665 CKRecord* modifiedRecord = [record copy];
666 modifiedRecord[SecCKRecordServerWasCurrent] = [NSNumber numberWithInteger:10];
667 [self.keychainZone addToZone:modifiedRecord];
669 [self.keychainView notifyZoneChange:nil];
670 [self.keychainView waitForFetchAndIncomingQueueProcessing];
672 // Check that the number is on the CKKSMirrorEntry
673 [self.keychainView dispatchSync: ^bool {
674 NSError* error = nil;
675 CKKSMirrorEntry* ckme = [CKKSMirrorEntry fromDatabase:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID error:&error];
677 XCTAssertNil(error, "no error fetching ckme");
678 XCTAssertNotNil(ckme, "Received a ckme");
680 XCTAssertEqual(ckme.wasCurrent, 10u, "Properly received wasCurrent");
686 -(void)testPCSCurrentPointerWriteFailure {
687 SecResetLocalSecuritydXPCFakeEntitlements();
688 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
689 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
690 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
692 NSNumber* servIdentifier = @3;
693 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
694 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
696 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
697 [self startCKKSSubsystem];
699 // Let things shake themselves out.
700 [self.keychainView waitForKeyHierarchyReadiness];
702 // Ensure there's no current pointer
703 [self fetchCurrentPointerExpectingError:false];
705 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
706 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
707 PCSServiceIdentifier:(NSNumber *)servIdentifier
708 PCSPublicKey:publicKey
709 PCSPublicIdentity:publicIdentity]];
711 NSDictionary* result = [self pcsAddItem:@"testaccount"
712 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
713 serviceIdentifier:(NSNumber*)servIdentifier
714 publicKey:(NSData*)publicKey
715 publicIdentity:(NSData*)publicIdentity
717 XCTAssertNotNil(result, "Received result from adding item");
719 // Check that the record is where we expect it in CloudKit
720 [self waitForCKModifications];
721 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
722 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
723 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
725 NSData* persistentRef = result[(id)kSecValuePersistentRef];
726 NSData* sha1 = result[(id)kSecAttrSHA1];
728 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
730 // Set the 'current' pointer.
731 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
733 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
734 (__bridge CFStringRef)@"pcsservice",
735 (__bridge CFStringRef)@"keychain",
736 (__bridge CFDataRef)persistentRef,
737 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
738 NSError* error = (__bridge NSError*)cferror;
739 XCTAssertNotNil(error, "Error setting current item when the write fails");
740 [setCurrentExpectation fulfill];
742 OCMVerifyAllWithDelay(self.mockDatabase, 8);
744 [self waitForExpectationsWithTimeout:8.0 handler:nil];
746 SecResetLocalSecuritydXPCFakeEntitlements();