]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTests+CurrentPointerAPI.m
Security-59306.61.1.tar.gz
[apple/security.git] / keychain / ckks / tests / CKKSTests+CurrentPointerAPI.m
1 /*
2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #if OCTAGON
25
26 #import <CloudKit/CloudKit.h>
27 #import <XCTest/XCTest.h>
28 #import <OCMock/OCMock.h>
29
30 #include <Security/SecItemPriv.h>
31 #include <Security/SecEntitlements.h>
32 #include <ipc/server_security_helpers.h>
33
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"
39
40 #import "keychain/ckks/tests/MockCloudKit.h"
41 #import "keychain/ckks/tests/AutoreleaseTest.h"
42
43 #import "keychain/ckks/tests/CKKSTests.h"
44 #import "keychain/ckks/tests/CKKSTests+API.h"
45
46 @interface CloudKitKeychainSyncingCurrentPointerAPITests : CloudKitKeychainSyncingTestsBase
47 @end
48
49 @implementation CloudKitKeychainSyncingCurrentPointerAPITests
50
51 -(void)fetchCurrentPointer:(bool)cached persistentRef:(NSData*)persistentRef
52 {
53 XCTestExpectation* currentExpectation = [self expectationWithDescription: @"callback occurs"];
54 SecItemFetchCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
55 (__bridge CFStringRef)@"pcsservice",
56 (__bridge CFStringRef)@"keychain",
57 cached,
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];
63 });
64 [self waitForExpectationsWithTimeout:8.0 handler:nil];
65 }
66 -(void)fetchCurrentPointerExpectingError:(bool)fetchCloudValue
67 {
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",
73 fetchCloudValue,
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];
78 });
79 TEST_API_AUTORELEASE_AFTER(SecItemFetchCurrentItemAcrossAllDevices);
80 [self waitForExpectationsWithTimeout:8.0 handler:nil];
81 }
82
83 - (void)testPCSFetchCurrentPointerCachedAndUncached {
84 SecResetLocalSecuritydXPCFakeEntitlements();
85 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
86 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
87 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
88
89 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
90 [self startCKKSSubsystem];
91
92 [self.keychainView waitForKeyHierarchyReadiness];
93 [self.keychainView waitUntilAllOperationsAreFinished]; // ensure everything finishes before we disallow fetches
94
95 // Ensure that local queries don't hit the server.
96 self.silentFetchesAllowed = false;
97 [self fetchCurrentPointerExpectingError:false];
98
99 // And ensure that global queries do.
100 [self expectCKFetch];
101 [self fetchCurrentPointerExpectingError:true];
102 OCMVerifyAllWithDelay(self.mockDatabase, 20);
103 SecResetLocalSecuritydXPCFakeEntitlements();
104 }
105
106 - (void)testPCSCurrentPointerAddAndUpdate {
107 SecResetLocalSecuritydXPCFakeEntitlements();
108 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
109 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
110 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
111
112 NSNumber* servIdentifier = @3;
113 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
114 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
115
116 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
117 [self startCKKSSubsystem];
118
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]];
122
123 // Ensure there's no current pointer
124 [self fetchCurrentPointerExpectingError:false];
125
126 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
127
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]];
133
134 NSDictionary* result = [self pcsAddItem:@"testaccount"
135 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
136 serviceIdentifier:(NSNumber*)servIdentifier
137 publicKey:(NSData*)publicKey
138 publicIdentity:(NSData*)publicIdentity
139 expectingSync:true];
140 XCTAssertNotNil(result, "Received result from adding item");
141 [self waitForExpectations:@[keychainChanged] timeout:8];
142
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");
148
149 NSData* persistentRef = result[(id)kSecValuePersistentRef];
150 NSData* sha1 = result[(id)kSecAttrSHA1];
151
152 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
153 deletedRecordTypeCounts:nil
154 zoneID:self.keychainZoneID
155 checkModifiedRecord:nil
156 runAfterModification:nil];
157
158 // Set the 'current' pointer.
159 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
160
161 // Ensure that setting the current pointer sends a notification
162 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
163
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];
173 });
174 TEST_API_AUTORELEASE_AFTER(SecItemSetCurrentItemAcrossAllDevices);
175 OCMVerifyAllWithDelay(self.mockDatabase, 20);
176 [self waitForExpectations:@[keychainChanged] timeout:8];
177 [self waitForCKModifications];
178
179 [self waitForExpectationsWithTimeout:8.0 handler:nil];
180
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");
185
186 // Check that the status APIs return the right value
187 [self fetchCurrentPointer:false persistentRef:persistentRef];
188
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];
191
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]];
197
198 result = [self pcsAddItem:@"tOTHER-ITEM"
199 data:[@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding]
200 serviceIdentifier:(NSNumber*)servIdentifier
201 publicKey:(NSData*)publicKey
202 publicIdentity:(NSData*)publicIdentity
203 expectingSync:true];
204 XCTAssertNotNil(result, "Received result from adding item");
205 [self waitForExpectations:@[keychainChanged] timeout:8];
206
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");
212
213 NSData* otherPersistentRef = result[(id)kSecValuePersistentRef];
214 NSData* otherSha1 = result[(id)kSecAttrSHA1];
215
216 // change the 'current' pointer.
217
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");
224
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);
229
230 if(![actualSHA1 isEqual:sha1]) {
231 secnotice("ckks", "SHA1s don't match, but why?");
232 }
233
234 XCTestExpectation* otherSetCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
235
236 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
237 deletedRecordTypeCounts:nil
238 zoneID:self.keychainZoneID
239 checkModifiedRecord:nil
240 runAfterModification:nil];
241
242 // Ensure that setting the current pointer sends a notification
243 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
244
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];
255 });
256 OCMVerifyAllWithDelay(self.mockDatabase, 20);
257 [self waitForExpectations:@[keychainChanged] timeout:8];
258 [self waitForCKModifications];
259
260 [self waitForExpectationsWithTimeout:8.0 handler:nil];
261
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");
266
267 // And: again
268 [self fetchCurrentPointer:false persistentRef:otherPersistentRef];
269 [self fetchCurrentPointer:true persistentRef:otherPersistentRef];
270
271 SecResetLocalSecuritydXPCFakeEntitlements();
272 }
273
274 - (void)testPCSCurrentPointerAddMissingItem {
275 SecResetLocalSecuritydXPCFakeEntitlements();
276 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
277 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
278 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
279
280 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
281 [self startCKKSSubsystem];
282
283 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
284
285 [self fetchCurrentPointerExpectingError:false];
286
287 NSData* fakepersistentRef = [@"not a real pref" dataUsingEncoding:NSUTF8StringEncoding];
288 NSData* fakesha1 = [@"not a real sha1" dataUsingEncoding:NSUTF8StringEncoding];
289
290 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
291
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];
301 });
302 TEST_API_AUTORELEASE_AFTER(SecItemSetCurrentItemAcrossAllDevices);
303 OCMVerifyAllWithDelay(self.mockDatabase, 20);
304 [self waitForCKModifications];
305
306 [self waitForExpectationsWithTimeout:8.0 handler:nil];
307
308 SecResetLocalSecuritydXPCFakeEntitlements();
309 }
310
311 - (void)testPCSCurrentPointerAddMissingOldItem {
312 SecResetLocalSecuritydXPCFakeEntitlements();
313 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
314 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
315 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
316
317 NSNumber* servIdentifier = @3;
318 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
319 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
320
321 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
322 [self startCKKSSubsystem];
323
324 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
325
326 [self fetchCurrentPointerExpectingError:false];
327
328 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
329
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]];
335
336 NSDictionary* result = [self pcsAddItem:@"testaccount"
337 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
338 serviceIdentifier:(NSNumber*)servIdentifier
339 publicKey:(NSData*)publicKey
340 publicIdentity:(NSData*)publicIdentity
341 expectingSync:true];
342 XCTAssertNotNil(result, "Received result from adding item");
343 [self waitForExpectations:@[keychainChanged] timeout:8];
344
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");
350
351 NSData* persistentRef = result[(id)kSecValuePersistentRef];
352 NSData* sha1 = result[(id)kSecAttrSHA1];
353
354 // Set the 'current' pointer.
355 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
356
357 NSData* fakepersistentRef = [@"not a real pref" dataUsingEncoding:NSUTF8StringEncoding];
358 NSData* fakesha1 = [@"not a real sha1" dataUsingEncoding:NSUTF8StringEncoding];
359
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];
372 });
373 TEST_API_AUTORELEASE_AFTER(SecItemSetCurrentItemAcrossAllDevices);
374 OCMVerifyAllWithDelay(self.mockDatabase, 20);
375 [self waitForCKModifications];
376
377 [self waitForExpectations:@[setCurrentExpectation] timeout:8];
378
379 SecResetLocalSecuritydXPCFakeEntitlements();
380 }
381
382 - (void)testPCSCurrentPointerAddNoCloudKitAccount {
383 SecResetLocalSecuritydXPCFakeEntitlements();
384 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
385 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
386 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
387
388 NSNumber* servIdentifier = @3;
389 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
390 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
391
392 // Entirely signed out of iCloud. all current record writes should fail.
393 self.accountStatus = CKAccountStatusNoAccount;
394
395 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
396 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
397
398 self.silentFetchesAllowed = false;
399 [self startCKKSSubsystem];
400
401 // Ensure there's no current pointer
402 [self fetchCurrentPointerExpectingError:false];
403
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");
412
413 NSData* persistentRef = result[(id)kSecValuePersistentRef];
414 NSData* sha1 = result[(id)kSecAttrSHA1];
415
416 // Set the 'current' pointer. This should fail.
417 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
418
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];
427 });
428
429 [self waitForExpectationsWithTimeout:8.0 handler:nil];
430 SecResetLocalSecuritydXPCFakeEntitlements();
431 }
432
433 - (void)testPCSCurrentPointerAddNonSyncItem {
434 SecResetLocalSecuritydXPCFakeEntitlements();
435 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
436 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
437 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
438
439 NSNumber* servIdentifier = @3;
440 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
441 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
442
443 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
444 [self startCKKSSubsystem];
445 [self.keychainView waitForKeyHierarchyReadiness];
446
447 NSMutableDictionary* query = [self pcsAddItemQuery:@"account"
448 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
449 serviceIdentifier:servIdentifier
450 publicKey:publicKey
451 publicIdentity:publicIdentity];
452 query[(id)kSecAttrSynchronizable] = @NO;
453
454 CFTypeRef cfresult = NULL;
455 XCTestExpectation* syncExpectation = [self expectationWithDescription: @"_SecItemAddAndNotifyOnSync callback occured"];
456
457 // Note that you will NOT receive a notification here, since you're adding a nonsync item
458 syncExpectation.inverted = YES;
459
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");
463
464 [syncExpectation fulfill];
465 }), @"_SecItemAddAndNotifyOnSync succeeded");
466
467 // We don't expect this callback to fire, so give it a second or so
468 [self waitForExpectations:@[syncExpectation] timeout:20];
469
470 NSDictionary* result = CFBridgingRelease(cfresult);
471
472 XCTAssertNotNil(result, "Received result from adding item");
473
474 NSData* persistentRef = result[(id)kSecValuePersistentRef];
475 NSData* sha1 = result[(id)kSecAttrSHA1];
476
477 // Set the 'current' pointer. This should fail.
478 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
479
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];
488 });
489
490 [self waitForExpectations:@[setCurrentExpectation] timeout:20];
491 SecResetLocalSecuritydXPCFakeEntitlements();
492 }
493
494 - (void)testPCSCurrentPointerReceive {
495 SecResetLocalSecuritydXPCFakeEntitlements();
496 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
497 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
498 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
499
500 NSNumber* servIdentifier = @3;
501 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
502 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
503
504 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
505 [self startCKKSSubsystem];
506
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]];
510
511 // Ensure there's no current pointer
512 [self fetchCurrentPointerExpectingError:false];
513
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]];
519
520 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
521
522 NSDictionary* result = [self pcsAddItem:@"testaccount"
523 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
524 serviceIdentifier:(NSNumber*)servIdentifier
525 publicKey:(NSData*)publicKey
526 publicIdentity:(NSData*)publicIdentity
527 expectingSync:true];
528 XCTAssertNotNil(result, "Received result from adding item");
529 NSData* persistentRef = result[(id)kSecValuePersistentRef];
530
531 [self waitForExpectations:@[keychainChanged] timeout:8];
532
533 // And a second item
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
544 expectingSync:true];
545 XCTAssertNotNil(result, "Received result from adding item");
546 NSData* persistentRef2 = result[(id)kSecValuePersistentRef];
547
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");
553
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");
557
558 // Still no current pointer.
559 [self fetchCurrentPointerExpectingError:false];
560
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");
571
572 // Ensure that receiving the current item pointer generates a notification
573 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
574
575 [self.keychainView notifyZoneChange:nil];
576 [self.keychainView waitForFetchAndIncomingQueueProcessing];
577
578 [self waitForExpectations:@[keychainChanged] timeout:8];
579 [self fetchCurrentPointer:false persistentRef:persistentRef];
580
581 // And again!
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");
591
592 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
593
594 [self.keychainView notifyZoneChange:nil];
595 [self.keychainView waitForFetchAndIncomingQueueProcessing];
596
597 [self waitForExpectations:@[keychainChanged] timeout:8];
598 [self fetchCurrentPointer:false persistentRef:persistentRef2];
599
600 SecResetLocalSecuritydXPCFakeEntitlements();
601 }
602
603
604 - (void)testPCSCurrentPointerReceiveDelete {
605 SecResetLocalSecuritydXPCFakeEntitlements();
606 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
607 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
608 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
609
610 NSNumber* servIdentifier = @3;
611 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
612 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
613
614 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
615 [self startCKKSSubsystem];
616
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]];
620
621 // Ensure there's no current pointer
622 [self fetchCurrentPointerExpectingError:false];
623
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]];
629
630 NSDictionary* result = [self pcsAddItem:@"testaccount"
631 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
632 serviceIdentifier:(NSNumber*)servIdentifier
633 publicKey:(NSData*)publicKey
634 publicIdentity:(NSData*)publicIdentity
635 expectingSync:true];
636 XCTAssertNotNil(result, "Received result from adding item");
637 NSData* persistentRef = result[(id)kSecValuePersistentRef];
638
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");
644
645 // Still no current pointer.
646 [self fetchCurrentPointerExpectingError:false];
647
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");
658
659 [self.keychainView notifyZoneChange:nil];
660 [self.keychainView waitForFetchAndIncomingQueueProcessing];
661
662 [self fetchCurrentPointer:false persistentRef:persistentRef];
663
664 // Ensure that receiving the current item pointer generates a notification
665 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
666
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];
672
673 [self fetchCurrentPointerExpectingError:false];
674
675 SecResetLocalSecuritydXPCFakeEntitlements();
676 }
677
678
679 - (void)testPCSCurrentPointerRecoverFromRecordExistsError {
680 SecResetLocalSecuritydXPCFakeEntitlements();
681 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
682 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
683 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
684
685 NSNumber* servIdentifier = @3;
686 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
687 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
688
689 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
690 [self startCKKSSubsystem];
691
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]];
695
696 // Ensure there's no current pointer
697 [self fetchCurrentPointerExpectingError:false];
698
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]];
704
705 NSDictionary* result = [self pcsAddItem:@"testaccount"
706 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
707 serviceIdentifier:(NSNumber*)servIdentifier
708 publicKey:(NSData*)publicKey
709 publicIdentity:(NSData*)publicIdentity
710 expectingSync:true];
711 XCTAssertNotNil(result, "Received result from adding item");
712
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");
719
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];
730
731 NSData* persistentRef = result[(id)kSecValuePersistentRef];
732 NSData* sha1 = result[(id)kSecAttrSHA1];
733
734 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
735
736 // Set the 'current' pointer.
737 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
738
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];
747 });
748 OCMVerifyAllWithDelay(self.mockDatabase, 20);
749 [self waitForCKModifications];
750 [self waitForExpectationsWithTimeout:8.0 handler:nil];
751
752 [self.keychainView waitUntilAllOperationsAreFinished];
753
754 [self fetchCurrentPointer:false persistentRef:persistentRef];
755
756 SecResetLocalSecuritydXPCFakeEntitlements();
757 }
758
759 - (void)testPCSCurrentPointerWasCurrent {
760 SecResetLocalSecuritydXPCFakeEntitlements();
761 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
762 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
763 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
764
765 NSNumber* servIdentifier = @3;
766 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
767 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
768
769 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
770 [self startCKKSSubsystem];
771
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]];
775
776 // Ensure there's no current pointer
777 [self fetchCurrentPointerExpectingError:false];
778
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]];
784
785 NSDictionary* result = [self pcsAddItem:@"testaccount"
786 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
787 serviceIdentifier:(NSNumber*)servIdentifier
788 publicKey:(NSData*)publicKey
789 publicIdentity:(NSData*)publicIdentity
790 expectingSync:true];
791 XCTAssertNotNil(result, "Received result from adding item");
792
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");
798
799 NSData* persistentRef = result[(id)kSecValuePersistentRef];
800 NSData* sha1 = result[(id)kSecAttrSHA1];
801
802 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
803 deletedRecordTypeCounts:nil
804 zoneID:self.keychainZoneID
805 checkModifiedRecord:nil
806 runAfterModification:nil];
807
808 // Set the 'current' pointer.
809 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
810
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];
819 });
820 OCMVerifyAllWithDelay(self.mockDatabase, 20);
821 [self waitForExpectationsWithTimeout:8.0 handler:nil];
822 [self waitForCKModifications];
823
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];
828
829 [self.keychainView notifyZoneChange:nil];
830 [self.keychainView waitForFetchAndIncomingQueueProcessing];
831
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];
836
837 XCTAssertNil(error, "no error fetching ckme");
838 XCTAssertNotNil(ckme, "Received a ckme");
839
840 XCTAssertEqual(ckme.wasCurrent, 10u, "Properly received wasCurrent");
841
842 return true;
843 }];
844 }
845
846 -(void)testPCSCurrentPointerWriteFailure {
847 SecResetLocalSecuritydXPCFakeEntitlements();
848 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
849 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
850 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
851
852 NSNumber* servIdentifier = @3;
853 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
854 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
855
856 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
857 [self startCKKSSubsystem];
858
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]];
862
863 // Ensure there's no current pointer
864 [self fetchCurrentPointerExpectingError:false];
865
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]];
871
872 NSDictionary* result = [self pcsAddItem:@"testaccount"
873 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
874 serviceIdentifier:(NSNumber*)servIdentifier
875 publicKey:(NSData*)publicKey
876 publicIdentity:(NSData*)publicIdentity
877 expectingSync:true];
878 XCTAssertNotNil(result, "Received result from adding item");
879
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");
885
886 NSData* persistentRef = result[(id)kSecValuePersistentRef];
887 NSData* sha1 = result[(id)kSecAttrSHA1];
888
889 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
890
891 // Set the 'current' pointer.
892 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
893
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];
902 });
903 OCMVerifyAllWithDelay(self.mockDatabase, 40);
904
905 [self waitForExpectationsWithTimeout:8.0 handler:nil];
906
907 SecResetLocalSecuritydXPCFakeEntitlements();
908 }
909
910 - (void)testPCSCurrentRecoverFromDanglingPointer {
911 SecResetLocalSecuritydXPCFakeEntitlements();
912 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
913 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
914 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
915
916 NSNumber* servIdentifier = @3;
917 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
918 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
919
920 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
921 [self startCKKSSubsystem];
922
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]];
926
927 // Ensure there's no current pointer
928 [self fetchCurrentPointerExpectingError:false];
929
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]];
935
936 NSDictionary* result = [self pcsAddItem:@"testaccount"
937 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
938 serviceIdentifier:(NSNumber*)servIdentifier
939 publicKey:(NSData*)publicKey
940 publicIdentity:(NSData*)publicIdentity
941 expectingSync:true];
942 XCTAssertNotNil(result, "Received result from adding item");
943
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");
949
950 NSData* persistentRef = result[(id)kSecValuePersistentRef];
951 NSData* sha1 = result[(id)kSecAttrSHA1];
952
953 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
954 deletedRecordTypeCounts:nil
955 zoneID:self.keychainZoneID
956 checkModifiedRecord:nil
957 runAfterModification:nil];
958
959 // Set the 'current' pointer.
960 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
961
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];
971 });
972 OCMVerifyAllWithDelay(self.mockDatabase, 20);
973 [self waitForCKModifications];
974
975 [self waitForExpectationsWithTimeout:8.0 handler:nil];
976
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);
986
987 // Now, fetch the current pointer: we should get an error
988 [self fetchCurrentPointerExpectingError:false];
989
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]];
996
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");
1004
1005 persistentRef = result[(id)kSecValuePersistentRef];
1006 sha1 = result[(id)kSecAttrSHA1];
1007
1008 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
1009 deletedRecordTypeCounts:nil
1010 zoneID:self.keychainZoneID
1011 checkModifiedRecord:nil
1012 runAfterModification:nil];
1013
1014 // Set the 'current' pointer.
1015 setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
1016
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];
1026 });
1027 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1028 [self waitForCKModifications];
1029 [self waitForExpectationsWithTimeout:8.0 handler:nil];
1030
1031 SecResetLocalSecuritydXPCFakeEntitlements();
1032 }
1033
1034 -(void)testPCSCurrentSetConflictedItemAsCurrent {
1035 SecResetLocalSecuritydXPCFakeEntitlements();
1036 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
1037 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
1038 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
1039
1040 NSNumber* servIdentifier = @3;
1041 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
1042 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
1043
1044 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1045 [self startCKKSSubsystem];
1046
1047 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1048 [self.keychainView waitUntilAllOperationsAreFinished];
1049
1050 // Before CKKS can add the item, shove a conflicting one into CloudKit
1051 NSError* error = nil;
1052
1053 NSString* account = @"testaccount";
1054
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
1058 options:0
1059 format:nil
1060 error:&error] mutableCopy];
1061 XCTAssertNil(error, "Error should be nil parsing base64 item");
1062
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];
1067
1068 self.keychainView.holdIncomingQueueOperation = [CKKSResultOperation named:@"hold-incoming" withBlock:^{
1069 secnotice("ckks", "Releasing process incoming queue hold");
1070 }];
1071
1072 NSData* firstItemData = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding];
1073
1074 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1075 NSDictionary* result = [self pcsAddItem:account
1076 data:firstItemData
1077 serviceIdentifier:(NSNumber*)servIdentifier
1078 publicKey:(NSData*)publicKey
1079 publicIdentity:(NSData*)publicIdentity
1080 expectingSync:false];
1081 XCTAssertNotNil(result, "Should receive result from adding item");
1082
1083 NSData* persistentRef = result[(id)kSecValuePersistentRef];
1084 NSData* sha1 = result[(id)kSecAttrSHA1];
1085
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,
1092 };
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);
1096 prefresult = NULL;
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");
1100
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,
1108 };
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");
1114
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];
1124 });
1125
1126 [self waitForExpectations:@[setCurrentExpectation] timeout:20];
1127
1128 // Now, release the incoming queue processing and retry the failure
1129 [self.operationQueue addOperation:self.keychainView.holdIncomingQueueOperation];
1130 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
1131
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];
1140 });
1141
1142 [self waitForExpectations:@[setCurrentExpectation] timeout:20];
1143
1144 // Reissue a fetch and find the new persistent ref and sha1 for the item at this UUID
1145 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1146
1147 // The conflicting item update should have won
1148 [self checkGenericPassword:@"conflictingdata" account:account];
1149
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,
1157 };
1158
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");
1163
1164 NSData* newPersistentRef = newResult[(id)kSecValuePersistentRef];
1165 NSData* newSha1 = newResult[(id)kSecAttrSHA1];
1166
1167 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
1168 deletedRecordTypeCounts:nil
1169 zoneID:self.keychainZoneID
1170 checkModifiedRecord:nil
1171 runAfterModification:nil];
1172
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];
1181 });
1182
1183 [self waitForExpectations:@[newSetCurrentExpectation] timeout:20];
1184
1185 SecResetLocalSecuritydXPCFakeEntitlements();
1186 }
1187
1188 @end
1189
1190 #endif // OCTAGON