]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTests+CurrentPointerAPI.m
Security-58286.200.222.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 [self.keychainView waitForKeyHierarchyReadiness];
121
122 // Ensure there's no current pointer
123 [self fetchCurrentPointerExpectingError:false];
124
125 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
126
127 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
128 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
129 PCSServiceIdentifier:(NSNumber *)servIdentifier
130 PCSPublicKey:publicKey
131 PCSPublicIdentity:publicIdentity]];
132
133 NSDictionary* result = [self pcsAddItem:@"testaccount"
134 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
135 serviceIdentifier:(NSNumber*)servIdentifier
136 publicKey:(NSData*)publicKey
137 publicIdentity:(NSData*)publicIdentity
138 expectingSync:true];
139 XCTAssertNotNil(result, "Received result from adding item");
140 [self waitForExpectations:@[keychainChanged] timeout:8];
141
142 // Check that the record is where we expect it in CloudKit
143 [self waitForCKModifications];
144 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
145 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
146 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
147
148 NSData* persistentRef = result[(id)kSecValuePersistentRef];
149 NSData* sha1 = result[(id)kSecAttrSHA1];
150
151 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
152 deletedRecordTypeCounts:nil
153 zoneID:self.keychainZoneID
154 checkModifiedRecord:nil
155 runAfterModification:nil];
156
157 // Set the 'current' pointer.
158 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
159
160 // Ensure that setting the current pointer sends a notification
161 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
162
163 TEST_API_AUTORELEASE_BEFORE(SecItemSetCurrentItemAcrossAllDevices);
164 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
165 (__bridge CFStringRef)@"pcsservice",
166 (__bridge CFStringRef)@"keychain",
167 (__bridge CFDataRef)persistentRef,
168 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
169 NSError* error = (__bridge NSError*)cferror;
170 XCTAssertNil(error, "No error setting current item");
171 [setCurrentExpectation fulfill];
172 });
173 TEST_API_AUTORELEASE_AFTER(SecItemSetCurrentItemAcrossAllDevices);
174 OCMVerifyAllWithDelay(self.mockDatabase, 20);
175 [self waitForExpectations:@[keychainChanged] timeout:8];
176 [self waitForCKModifications];
177
178 [self waitForExpectationsWithTimeout:8.0 handler:nil];
179
180 CKRecord* currentItemPointer = self.keychainZone.currentDatabase[[[CKRecordID alloc] initWithRecordName:@"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID]];
181 XCTAssertNotNil(currentItemPointer, "Found a CKRecord at the expected location in CloudKit");
182 XCTAssertEqualObjects(currentItemPointer.recordType, SecCKRecordCurrentItemType, "Saved CKRecord is correct type");
183 XCTAssertEqualObjects(((CKReference*)currentItemPointer[SecCKRecordItemRefKey]).recordID, pcsItemRecordID, "Current Item record points to correct record");
184
185 // Check that the status APIs return the right value
186 [self fetchCurrentPointer:false persistentRef:persistentRef];
187
188 // Rad. If we got here, adding a new current item pointer works. Let's see if we can modify one.
189 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
190
191 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
192 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
193 PCSServiceIdentifier:(NSNumber *)servIdentifier
194 PCSPublicKey:publicKey
195 PCSPublicIdentity:publicIdentity]];
196
197 result = [self pcsAddItem:@"tOTHER-ITEM"
198 data:[@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding]
199 serviceIdentifier:(NSNumber*)servIdentifier
200 publicKey:(NSData*)publicKey
201 publicIdentity:(NSData*)publicIdentity
202 expectingSync:true];
203 XCTAssertNotNil(result, "Received result from adding item");
204 [self waitForExpectations:@[keychainChanged] timeout:8];
205
206 // Check that the record is where we expect it
207 [self waitForCKModifications];
208 CKRecordID* pcsOtherItemRecordID = [[CKRecordID alloc] initWithRecordName: @"878BEAA6-1EE9-1079-1025-E6832AC8F2F3" zoneID:self.keychainZoneID];
209 CKRecord* recordOther = self.keychainZone.currentDatabase[pcsOtherItemRecordID];
210 XCTAssertNotNil(recordOther, "Found other record in CloudKit at expected UUID");
211
212 NSData* otherPersistentRef = result[(id)kSecValuePersistentRef];
213 NSData* otherSha1 = result[(id)kSecAttrSHA1];
214
215 // change the 'current' pointer.
216
217 // Refetch the old item's hash, just in case it's changed (it does, about 50% of the time. I'm not sure why).
218 CFTypeRef cfresult = NULL;
219 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) @{
220 (id)kSecValuePersistentRef : persistentRef,
221 (id)kSecReturnAttributes : @YES,
222 }, &cfresult), "Found original item by persistent reference");
223
224 XCTAssertNotNil((__bridge id)cfresult, "Received an item by finding persistent reference");
225 NSData* actualSHA1 = CFBridgingRelease(CFRetainSafe(CFDictionaryGetValue(cfresult, kSecAttrSHA1)));
226 XCTAssertNotNil(actualSHA1, "Have a SHA1 for the original item");
227 CFReleaseNull(cfresult);
228
229 if(![actualSHA1 isEqual:sha1]) {
230 secnotice("ckks", "SHA1s don't match, but why?");
231 }
232
233 XCTestExpectation* otherSetCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
234
235 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
236 deletedRecordTypeCounts:nil
237 zoneID:self.keychainZoneID
238 checkModifiedRecord:nil
239 runAfterModification:nil];
240
241 // Ensure that setting the current pointer sends a notification
242 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
243
244 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
245 (__bridge CFStringRef)@"pcsservice",
246 (__bridge CFStringRef)@"keychain",
247 (__bridge CFDataRef)otherPersistentRef,
248 (__bridge CFDataRef)otherSha1,
249 (__bridge CFDataRef)persistentRef,
250 (__bridge CFDataRef)actualSHA1, ^ (CFErrorRef cferror) {
251 NSError* error = (__bridge NSError*)cferror;
252 XCTAssertNil(error, "No error setting current item");
253 [otherSetCurrentExpectation fulfill];
254 });
255 OCMVerifyAllWithDelay(self.mockDatabase, 20);
256 [self waitForExpectations:@[keychainChanged] timeout:8];
257 [self waitForCKModifications];
258
259 [self waitForExpectationsWithTimeout:8.0 handler:nil];
260
261 currentItemPointer = self.keychainZone.currentDatabase[[[CKRecordID alloc] initWithRecordName:@"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID]];
262 XCTAssertNotNil(currentItemPointer, "Found a CKRecord at the expected location in CloudKit");
263 XCTAssertEqualObjects(currentItemPointer.recordType, SecCKRecordCurrentItemType, "Saved CKRecord is correct type");
264 XCTAssertEqualObjects(((CKReference*)currentItemPointer[SecCKRecordItemRefKey]).recordID, pcsOtherItemRecordID, "Current Item record points to updated record");
265
266 // And: again
267 [self fetchCurrentPointer:false persistentRef:otherPersistentRef];
268 [self fetchCurrentPointer:true persistentRef:otherPersistentRef];
269
270 SecResetLocalSecuritydXPCFakeEntitlements();
271 }
272
273 - (void)testPCSCurrentPointerAddMissingItem {
274 SecResetLocalSecuritydXPCFakeEntitlements();
275 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
276 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
277 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
278
279 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
280 [self startCKKSSubsystem];
281
282 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
283
284 [self fetchCurrentPointerExpectingError:false];
285
286 NSData* fakepersistentRef = [@"not a real pref" dataUsingEncoding:NSUTF8StringEncoding];
287 NSData* fakesha1 = [@"not a real sha1" dataUsingEncoding:NSUTF8StringEncoding];
288
289 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
290
291 TEST_API_AUTORELEASE_BEFORE(SecItemSetCurrentItemAcrossAllDevices);
292 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
293 (__bridge CFStringRef)@"pcsservice",
294 (__bridge CFStringRef)@"keychain",
295 (__bridge CFDataRef)fakepersistentRef,
296 (__bridge CFDataRef)fakesha1, NULL, NULL, ^ (CFErrorRef cferror) {
297 NSError* error = (__bridge NSError*)cferror;
298 XCTAssertNotNil(error, "Should error setting current item to a nonexistent item");
299 [setCurrentExpectation fulfill];
300 });
301 TEST_API_AUTORELEASE_AFTER(SecItemSetCurrentItemAcrossAllDevices);
302 OCMVerifyAllWithDelay(self.mockDatabase, 20);
303 [self waitForCKModifications];
304
305 [self waitForExpectationsWithTimeout:8.0 handler:nil];
306
307 SecResetLocalSecuritydXPCFakeEntitlements();
308 }
309
310 - (void)testPCSCurrentPointerAddMissingOldItem {
311 SecResetLocalSecuritydXPCFakeEntitlements();
312 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
313 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
314 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
315
316 NSNumber* servIdentifier = @3;
317 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
318 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
319
320 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
321 [self startCKKSSubsystem];
322
323 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
324
325 [self fetchCurrentPointerExpectingError:false];
326
327 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
328
329 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
330 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
331 PCSServiceIdentifier:(NSNumber *)servIdentifier
332 PCSPublicKey:publicKey
333 PCSPublicIdentity:publicIdentity]];
334
335 NSDictionary* result = [self pcsAddItem:@"testaccount"
336 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
337 serviceIdentifier:(NSNumber*)servIdentifier
338 publicKey:(NSData*)publicKey
339 publicIdentity:(NSData*)publicIdentity
340 expectingSync:true];
341 XCTAssertNotNil(result, "Received result from adding item");
342 [self waitForExpectations:@[keychainChanged] timeout:8];
343
344 // Check that the record is where we expect it in CloudKit
345 [self waitForCKModifications];
346 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
347 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
348 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
349
350 NSData* persistentRef = result[(id)kSecValuePersistentRef];
351 NSData* sha1 = result[(id)kSecAttrSHA1];
352
353 // Set the 'current' pointer.
354 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
355
356 NSData* fakepersistentRef = [@"not a real pref" dataUsingEncoding:NSUTF8StringEncoding];
357 NSData* fakesha1 = [@"not a real sha1" dataUsingEncoding:NSUTF8StringEncoding];
358
359 TEST_API_AUTORELEASE_BEFORE(SecItemSetCurrentItemAcrossAllDevices);
360 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
361 (__bridge CFStringRef)@"pcsservice",
362 (__bridge CFStringRef)@"keychain",
363 (__bridge CFDataRef)persistentRef,
364 (__bridge CFDataRef)sha1,
365 (__bridge CFDataRef)fakepersistentRef,
366 (__bridge CFDataRef)fakesha1,
367 ^(CFErrorRef cferror) {
368 NSError* error = (__bridge NSError*)cferror;
369 XCTAssertNotNil(error, "Should error setting current item when passing garbage for old item");
370 [setCurrentExpectation fulfill];
371 });
372 TEST_API_AUTORELEASE_AFTER(SecItemSetCurrentItemAcrossAllDevices);
373 OCMVerifyAllWithDelay(self.mockDatabase, 20);
374 [self waitForCKModifications];
375
376 [self waitForExpectations:@[setCurrentExpectation] timeout:8];
377
378 SecResetLocalSecuritydXPCFakeEntitlements();
379 }
380
381 - (void)testPCSCurrentPointerAddNoCloudKitAccount {
382 SecResetLocalSecuritydXPCFakeEntitlements();
383 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
384 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
385 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
386
387 NSNumber* servIdentifier = @3;
388 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
389 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
390
391 // Entirely signed out of iCloud. all current record writes should fail.
392 self.accountStatus = CKAccountStatusNoAccount;
393 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCNotInCircle error:nil];
394 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
395
396 self.silentFetchesAllowed = false;
397 [self startCKKSSubsystem];
398
399 // Ensure there's no current pointer
400 [self fetchCurrentPointerExpectingError:false];
401
402 // Should NOT add an item to CloudKit
403 NSDictionary* result = [self pcsAddItem:@"testaccount"
404 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
405 serviceIdentifier:(NSNumber*)servIdentifier
406 publicKey:(NSData*)publicKey
407 publicIdentity:(NSData*)publicIdentity
408 expectingSync:false];
409 XCTAssertNotNil(result, "Received result from adding item");
410
411 NSData* persistentRef = result[(id)kSecValuePersistentRef];
412 NSData* sha1 = result[(id)kSecAttrSHA1];
413
414 // Set the 'current' pointer. This should fail.
415 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
416
417 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
418 (__bridge CFStringRef)@"pcsservice",
419 (__bridge CFStringRef)@"keychain",
420 (__bridge CFDataRef)persistentRef,
421 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
422 NSError* error = (__bridge NSError*)cferror;
423 XCTAssertNotNil(error, "Error setting current item with no CloudKit account");
424 [setCurrentExpectation fulfill];
425 });
426
427 [self waitForExpectationsWithTimeout:8.0 handler:nil];
428 SecResetLocalSecuritydXPCFakeEntitlements();
429 }
430
431 - (void)testPCSCurrentPointerAddNonSyncItem {
432 SecResetLocalSecuritydXPCFakeEntitlements();
433 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
434 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
435 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
436
437 NSNumber* servIdentifier = @3;
438 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
439 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
440
441 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
442 [self startCKKSSubsystem];
443 [self.keychainView waitForKeyHierarchyReadiness];
444
445 NSMutableDictionary* query = [self pcsAddItemQuery:@"account"
446 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
447 serviceIdentifier:servIdentifier
448 publicKey:publicKey
449 publicIdentity:publicIdentity];
450 query[(id)kSecAttrSynchronizable] = @NO;
451
452 CFTypeRef cfresult = NULL;
453 XCTestExpectation* syncExpectation = [self expectationWithDescription: @"_SecItemAddAndNotifyOnSync callback occured"];
454
455 // Note that you will NOT receive a notification here, since you're adding a nonsync item
456 syncExpectation.inverted = YES;
457
458 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, &cfresult, ^(bool didSync, CFErrorRef error) {
459 XCTAssertFalse(didSync, "Item did not sync");
460 XCTAssertNotNil((__bridge NSError*)error, "Error syncing item");
461
462 [syncExpectation fulfill];
463 }), @"_SecItemAddAndNotifyOnSync succeeded");
464
465 // We don't expect this callback to fire, so give it a second or so
466 [self waitForExpectations:@[syncExpectation] timeout:20];
467
468 NSDictionary* result = CFBridgingRelease(cfresult);
469
470 XCTAssertNotNil(result, "Received result from adding item");
471
472 NSData* persistentRef = result[(id)kSecValuePersistentRef];
473 NSData* sha1 = result[(id)kSecAttrSHA1];
474
475 // Set the 'current' pointer. This should fail.
476 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
477
478 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
479 (__bridge CFStringRef)@"pcsservice",
480 (__bridge CFStringRef)@"keychain",
481 (__bridge CFDataRef)persistentRef,
482 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
483 NSError* error = (__bridge NSError*)cferror;
484 XCTAssertNotNil(error, "Error setting current item to nonsyncable item");
485 [setCurrentExpectation fulfill];
486 });
487
488 [self waitForExpectations:@[setCurrentExpectation] timeout:20];
489 SecResetLocalSecuritydXPCFakeEntitlements();
490 }
491
492 - (void)testPCSCurrentPointerReceive {
493 SecResetLocalSecuritydXPCFakeEntitlements();
494 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
495 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
496 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
497
498 NSNumber* servIdentifier = @3;
499 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
500 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
501
502 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
503 [self startCKKSSubsystem];
504
505 // Let things shake themselves out.
506 [self.keychainView waitForKeyHierarchyReadiness];
507
508 // Ensure there's no current pointer
509 [self fetchCurrentPointerExpectingError:false];
510
511 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
512 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
513 PCSServiceIdentifier:(NSNumber *)servIdentifier
514 PCSPublicKey:publicKey
515 PCSPublicIdentity:publicIdentity]];
516
517 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
518
519 NSDictionary* result = [self pcsAddItem:@"testaccount"
520 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
521 serviceIdentifier:(NSNumber*)servIdentifier
522 publicKey:(NSData*)publicKey
523 publicIdentity:(NSData*)publicIdentity
524 expectingSync:true];
525 XCTAssertNotNil(result, "Received result from adding item");
526 NSData* persistentRef = result[(id)kSecValuePersistentRef];
527
528 [self waitForExpectations:@[keychainChanged] timeout:8];
529
530 // And a second item
531 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
532 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
533 PCSServiceIdentifier:(NSNumber *)servIdentifier
534 PCSPublicKey:publicKey
535 PCSPublicIdentity:publicIdentity]];
536 result = [self pcsAddItem:@"testaccount2"
537 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
538 serviceIdentifier:(NSNumber*)servIdentifier
539 publicKey:(NSData*)publicKey
540 publicIdentity:(NSData*)publicIdentity
541 expectingSync:true];
542 XCTAssertNotNil(result, "Received result from adding item");
543 NSData* persistentRef2 = result[(id)kSecValuePersistentRef];
544
545 // Check that the records are where we expect them in CloudKit
546 [self waitForCKModifications];
547 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
548 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
549 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
550
551 CKRecordID* pcsItemRecordID2 = [[CKRecordID alloc] initWithRecordName: @"3AB8E78D-75AF-CFEF-F833-FA3E3E90978A" zoneID:self.keychainZoneID];
552 CKRecord* record2 = self.keychainZone.currentDatabase[pcsItemRecordID2];
553 XCTAssertNotNil(record2, "Found 2nd record in CloudKit at expected UUID");
554
555 // Still no current pointer.
556 [self fetchCurrentPointerExpectingError:false];
557
558 // Another machine comes along and updates the pointer!
559 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
560 currentItemUUID:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3"
561 state:SecCKKSProcessedStateRemote
562 zoneID:self.keychainZoneID
563 encodedCKRecord:nil];
564 [self.keychainZone addToZone: [cip CKRecordWithZoneID:self.keychainZoneID]];
565 CKRecordID* currentPointerRecordID = [[CKRecordID alloc] initWithRecordName: @"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID];
566 CKRecord* currentPointerRecord = self.keychainZone.currentDatabase[currentPointerRecordID];
567 XCTAssertNotNil(currentPointerRecord, "Found record in CloudKit at expected UUID");
568
569 // Ensure that receiving the current item pointer generates a notification
570 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
571
572 [self.keychainView notifyZoneChange:nil];
573 [self.keychainView waitForFetchAndIncomingQueueProcessing];
574
575 [self waitForExpectations:@[keychainChanged] timeout:8];
576 [self fetchCurrentPointer:false persistentRef:persistentRef];
577
578 // And again!
579 CKKSCurrentItemPointer* cip2 = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
580 currentItemUUID:pcsItemRecordID2.recordName
581 state:SecCKKSProcessedStateRemote
582 zoneID:self.keychainZoneID
583 encodedCKRecord:nil];
584 [self.keychainZone addToZone: [cip2 CKRecordWithZoneID:self.keychainZoneID]];
585 CKRecordID* currentPointerRecordID2 = [[CKRecordID alloc] initWithRecordName: @"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID];
586 CKRecord* currentPointerRecord2 = self.keychainZone.currentDatabase[currentPointerRecordID2];
587 XCTAssertNotNil(currentPointerRecord2, "Found record in CloudKit at expected UUID");
588
589 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
590
591 [self.keychainView notifyZoneChange:nil];
592 [self.keychainView waitForFetchAndIncomingQueueProcessing];
593
594 [self waitForExpectations:@[keychainChanged] timeout:8];
595 [self fetchCurrentPointer:false persistentRef:persistentRef2];
596
597 SecResetLocalSecuritydXPCFakeEntitlements();
598 }
599
600
601 - (void)testPCSCurrentPointerReceiveDelete {
602 SecResetLocalSecuritydXPCFakeEntitlements();
603 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
604 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
605 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
606
607 NSNumber* servIdentifier = @3;
608 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
609 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
610
611 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
612 [self startCKKSSubsystem];
613
614 // Let things shake themselves out.
615 [self.keychainView waitForKeyHierarchyReadiness];
616
617 // Ensure there's no current pointer
618 [self fetchCurrentPointerExpectingError:false];
619
620 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
621 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
622 PCSServiceIdentifier:(NSNumber *)servIdentifier
623 PCSPublicKey:publicKey
624 PCSPublicIdentity:publicIdentity]];
625
626 NSDictionary* result = [self pcsAddItem:@"testaccount"
627 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
628 serviceIdentifier:(NSNumber*)servIdentifier
629 publicKey:(NSData*)publicKey
630 publicIdentity:(NSData*)publicIdentity
631 expectingSync:true];
632 XCTAssertNotNil(result, "Received result from adding item");
633 NSData* persistentRef = result[(id)kSecValuePersistentRef];
634
635 // Check that the record is where we expect it in CloudKit
636 [self waitForCKModifications];
637 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
638 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
639 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
640
641 // Still no current pointer.
642 [self fetchCurrentPointerExpectingError:false];
643
644 // Another machine comes along and updates the pointer!
645 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
646 currentItemUUID:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3"
647 state:SecCKKSProcessedStateRemote
648 zoneID:self.keychainZoneID
649 encodedCKRecord:nil];
650 [self.keychainZone addToZone: [cip CKRecordWithZoneID:self.keychainZoneID]];
651 CKRecordID* currentPointerRecordID = [[CKRecordID alloc] initWithRecordName: @"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID];
652 CKRecord* currentPointerRecord = self.keychainZone.currentDatabase[currentPointerRecordID];
653 XCTAssertNotNil(currentPointerRecord, "Found record in CloudKit at expected UUID");
654
655 [self.keychainView notifyZoneChange:nil];
656 [self.keychainView waitForFetchAndIncomingQueueProcessing];
657
658 [self fetchCurrentPointer:false persistentRef:persistentRef];
659
660 // Ensure that receiving the current item pointer generates a notification
661 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
662
663 // Another machine comes along and deletes the pointer!
664 [self.keychainZone deleteCKRecordIDFromZone: currentPointerRecordID];
665 [self.keychainView notifyZoneChange:nil];
666 [self.keychainView waitForFetchAndIncomingQueueProcessing];
667 [self waitForExpectations:@[keychainChanged] timeout:8];
668
669 [self fetchCurrentPointerExpectingError:false];
670
671 SecResetLocalSecuritydXPCFakeEntitlements();
672 }
673
674
675 - (void)testPCSCurrentPointerRecoverFromRecordExistsError {
676 SecResetLocalSecuritydXPCFakeEntitlements();
677 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
678 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
679 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
680
681 NSNumber* servIdentifier = @3;
682 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
683 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
684
685 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
686 [self startCKKSSubsystem];
687
688 // Let things shake themselves out.
689 [self.keychainView waitForKeyHierarchyReadiness];
690
691 // Ensure there's no current pointer
692 [self fetchCurrentPointerExpectingError:false];
693
694 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
695 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
696 PCSServiceIdentifier:(NSNumber *)servIdentifier
697 PCSPublicKey:publicKey
698 PCSPublicIdentity:publicIdentity]];
699
700 NSDictionary* result = [self pcsAddItem:@"testaccount"
701 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
702 serviceIdentifier:(NSNumber*)servIdentifier
703 publicKey:(NSData*)publicKey
704 publicIdentity:(NSData*)publicIdentity
705 expectingSync:true];
706 XCTAssertNotNil(result, "Received result from adding item");
707
708 // Check that the record is where we expect it in CloudKit
709 [self waitForCKModifications];
710 NSString* recordUUID = @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3";
711 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName:recordUUID zoneID:self.keychainZoneID];
712 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
713 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
714
715 // Someone else sets the current record pointer
716 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
717 currentItemUUID:recordUUID
718 state:SecCKKSProcessedStateRemote
719 zoneID:self.keychainZoneID
720 encodedCKRecord:nil];
721 XCTAssertNotNil(cip, "Should have created a CIP");
722 CKRecord* cipRecord = [cip CKRecordWithZoneID:self.keychainZoneID];
723 XCTAssertNotNil(cipRecord, "Should have created a CKRecord for this CIP");
724 [self.keychainZone addToZone: cipRecord];
725
726 NSData* persistentRef = result[(id)kSecValuePersistentRef];
727 NSData* sha1 = result[(id)kSecAttrSHA1];
728
729 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
730
731 // Set the 'current' pointer.
732 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
733
734 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
735 (__bridge CFStringRef)@"pcsservice",
736 (__bridge CFStringRef)@"keychain",
737 (__bridge CFDataRef)persistentRef,
738 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
739 NSError* error = (__bridge NSError*)cferror;
740 XCTAssertNotNil(error, "Should have received an error setting current item (because of conflict)");
741 [setCurrentExpectation fulfill];
742 });
743 OCMVerifyAllWithDelay(self.mockDatabase, 20);
744 [self waitForCKModifications];
745 [self waitForExpectationsWithTimeout:8.0 handler:nil];
746
747 [self.keychainView waitUntilAllOperationsAreFinished];
748
749 [self fetchCurrentPointer:false persistentRef:persistentRef];
750
751 SecResetLocalSecuritydXPCFakeEntitlements();
752 }
753
754 - (void)testPCSCurrentPointerWasCurrent {
755 SecResetLocalSecuritydXPCFakeEntitlements();
756 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
757 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
758 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
759
760 NSNumber* servIdentifier = @3;
761 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
762 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
763
764 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
765 [self startCKKSSubsystem];
766
767 // Let things shake themselves out.
768 [self.keychainView waitForKeyHierarchyReadiness];
769
770 // Ensure there's no current pointer
771 [self fetchCurrentPointerExpectingError:false];
772
773 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
774 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
775 PCSServiceIdentifier:(NSNumber *)servIdentifier
776 PCSPublicKey:publicKey
777 PCSPublicIdentity:publicIdentity]];
778
779 NSDictionary* result = [self pcsAddItem:@"testaccount"
780 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
781 serviceIdentifier:(NSNumber*)servIdentifier
782 publicKey:(NSData*)publicKey
783 publicIdentity:(NSData*)publicIdentity
784 expectingSync:true];
785 XCTAssertNotNil(result, "Received result from adding item");
786
787 // Check that the record is where we expect it in CloudKit
788 [self waitForCKModifications];
789 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
790 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
791 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
792
793 NSData* persistentRef = result[(id)kSecValuePersistentRef];
794 NSData* sha1 = result[(id)kSecAttrSHA1];
795
796 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
797 deletedRecordTypeCounts:nil
798 zoneID:self.keychainZoneID
799 checkModifiedRecord:nil
800 runAfterModification:nil];
801
802 // Set the 'current' pointer.
803 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
804
805 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
806 (__bridge CFStringRef)@"pcsservice",
807 (__bridge CFStringRef)@"keychain",
808 (__bridge CFDataRef)persistentRef,
809 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
810 NSError* error = (__bridge NSError*)cferror;
811 XCTAssertNil(error, "No error setting current item");
812 [setCurrentExpectation fulfill];
813 });
814 OCMVerifyAllWithDelay(self.mockDatabase, 20);
815 [self waitForExpectationsWithTimeout:8.0 handler:nil];
816 [self waitForCKModifications];
817
818 // Set the 'was current' flag on the record
819 CKRecord* modifiedRecord = [record copy];
820 modifiedRecord[SecCKRecordServerWasCurrent] = [NSNumber numberWithInteger:10];
821 [self.keychainZone addToZone:modifiedRecord];
822
823 [self.keychainView notifyZoneChange:nil];
824 [self.keychainView waitForFetchAndIncomingQueueProcessing];
825
826 // Check that the number is on the CKKSMirrorEntry
827 [self.keychainView dispatchSync: ^bool {
828 NSError* error = nil;
829 CKKSMirrorEntry* ckme = [CKKSMirrorEntry fromDatabase:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID error:&error];
830
831 XCTAssertNil(error, "no error fetching ckme");
832 XCTAssertNotNil(ckme, "Received a ckme");
833
834 XCTAssertEqual(ckme.wasCurrent, 10u, "Properly received wasCurrent");
835
836 return true;
837 }];
838 }
839
840 -(void)testPCSCurrentPointerWriteFailure {
841 SecResetLocalSecuritydXPCFakeEntitlements();
842 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
843 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
844 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
845
846 NSNumber* servIdentifier = @3;
847 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
848 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
849
850 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
851 [self startCKKSSubsystem];
852
853 // Let things shake themselves out.
854 [self.keychainView waitForKeyHierarchyReadiness];
855
856 // Ensure there's no current pointer
857 [self fetchCurrentPointerExpectingError:false];
858
859 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
860 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
861 PCSServiceIdentifier:(NSNumber *)servIdentifier
862 PCSPublicKey:publicKey
863 PCSPublicIdentity:publicIdentity]];
864
865 NSDictionary* result = [self pcsAddItem:@"testaccount"
866 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
867 serviceIdentifier:(NSNumber*)servIdentifier
868 publicKey:(NSData*)publicKey
869 publicIdentity:(NSData*)publicIdentity
870 expectingSync:true];
871 XCTAssertNotNil(result, "Received result from adding item");
872
873 // Check that the record is where we expect it in CloudKit
874 [self waitForCKModifications];
875 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
876 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
877 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
878
879 NSData* persistentRef = result[(id)kSecValuePersistentRef];
880 NSData* sha1 = result[(id)kSecAttrSHA1];
881
882 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
883
884 // Set the 'current' pointer.
885 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
886
887 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
888 (__bridge CFStringRef)@"pcsservice",
889 (__bridge CFStringRef)@"keychain",
890 (__bridge CFDataRef)persistentRef,
891 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
892 NSError* error = (__bridge NSError*)cferror;
893 XCTAssertNotNil(error, "Error setting current item when the write fails");
894 [setCurrentExpectation fulfill];
895 });
896 OCMVerifyAllWithDelay(self.mockDatabase, 20);
897
898 [self waitForExpectationsWithTimeout:8.0 handler:nil];
899
900 SecResetLocalSecuritydXPCFakeEntitlements();
901 }
902
903 - (void)testPCSCurrentRecoverFromDanglingPointer {
904 SecResetLocalSecuritydXPCFakeEntitlements();
905 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
906 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
907 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
908
909 NSNumber* servIdentifier = @3;
910 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
911 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
912
913 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
914 [self startCKKSSubsystem];
915
916 // Let things shake themselves out.
917 [self.keychainView waitForKeyHierarchyReadiness];
918
919 // Ensure there's no current pointer
920 [self fetchCurrentPointerExpectingError:false];
921
922 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
923 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
924 PCSServiceIdentifier:(NSNumber *)servIdentifier
925 PCSPublicKey:publicKey
926 PCSPublicIdentity:publicIdentity]];
927
928 NSDictionary* result = [self pcsAddItem:@"testaccount"
929 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
930 serviceIdentifier:(NSNumber*)servIdentifier
931 publicKey:(NSData*)publicKey
932 publicIdentity:(NSData*)publicIdentity
933 expectingSync:true];
934 XCTAssertNotNil(result, "Received result from adding item");
935
936 // Check that the record is where we expect it in CloudKit
937 [self waitForCKModifications];
938 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
939 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
940 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
941
942 NSData* persistentRef = result[(id)kSecValuePersistentRef];
943 NSData* sha1 = result[(id)kSecAttrSHA1];
944
945 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
946 deletedRecordTypeCounts:nil
947 zoneID:self.keychainZoneID
948 checkModifiedRecord:nil
949 runAfterModification:nil];
950
951 // Set the 'current' pointer.
952 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
953
954 // Ensure that setting the current pointer sends a notification
955 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
956 (__bridge CFStringRef)@"pcsservice",
957 (__bridge CFStringRef)@"keychain",
958 (__bridge CFDataRef)persistentRef,
959 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
960 NSError* error = (__bridge NSError*)cferror;
961 XCTAssertNil(error, "No error setting current item");
962 [setCurrentExpectation fulfill];
963 });
964 OCMVerifyAllWithDelay(self.mockDatabase, 20);
965 [self waitForCKModifications];
966
967 [self waitForExpectationsWithTimeout:8.0 handler:nil];
968
969 // Delete the keychain item
970 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
971 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
972 (id)kSecClass : (id)kSecClassGenericPassword,
973 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
974 (id)kSecAttrAccount:@"testaccount",
975 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
976 }), "Should receive no error deleting item");
977 OCMVerifyAllWithDelay(self.mockDatabase, 20);
978
979 // Now, fetch the current pointer: we should get an error
980 [self fetchCurrentPointerExpectingError:false];
981
982 // Setting the current item pointer again, using a NULL old value, should work.
983 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
984 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
985 PCSServiceIdentifier:(NSNumber *)servIdentifier
986 PCSPublicKey:publicKey
987 PCSPublicIdentity:publicIdentity]];
988
989 result = [self pcsAddItem:@"testaccount2"
990 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
991 serviceIdentifier:(NSNumber*)servIdentifier
992 publicKey:(NSData*)publicKey
993 publicIdentity:(NSData*)publicIdentity
994 expectingSync:true];
995 XCTAssertNotNil(result, "Should have result from adding item2");
996
997 persistentRef = result[(id)kSecValuePersistentRef];
998 sha1 = result[(id)kSecAttrSHA1];
999
1000 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
1001 deletedRecordTypeCounts:nil
1002 zoneID:self.keychainZoneID
1003 checkModifiedRecord:nil
1004 runAfterModification:nil];
1005
1006 // Set the 'current' pointer.
1007 setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
1008
1009 // Ensure that setting the current pointer sends a notification
1010 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
1011 (__bridge CFStringRef)@"pcsservice",
1012 (__bridge CFStringRef)@"keychain",
1013 (__bridge CFDataRef)persistentRef,
1014 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
1015 NSError* error = (__bridge NSError*)cferror;
1016 XCTAssertNil(error, "No error setting current item");
1017 [setCurrentExpectation fulfill];
1018 });
1019 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1020 [self waitForCKModifications];
1021 [self waitForExpectationsWithTimeout:8.0 handler:nil];
1022
1023 SecResetLocalSecuritydXPCFakeEntitlements();
1024 }
1025
1026 -(void)testPCSCurrentSetConflictedItemAsCurrent {
1027 SecResetLocalSecuritydXPCFakeEntitlements();
1028 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
1029 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
1030 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
1031
1032 NSNumber* servIdentifier = @3;
1033 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
1034 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
1035
1036 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1037 [self startCKKSSubsystem];
1038
1039 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1040 [self.keychainView waitUntilAllOperationsAreFinished];
1041
1042 // Before CKKS can add the item, shove a conflicting one into CloudKit
1043 NSError* error = nil;
1044
1045 NSString* account = @"testaccount";
1046
1047 // Create an item in CloudKit that will conflict in both UUID and primary key
1048 NSData* itemdata = [[NSData alloc] initWithBase64EncodedString:@"YnBsaXN0MDDbAQIDBAUGBwgJCgsMDQ4PEBESEhMUFVZ2X0RhdGFUYWNjdFR0b21iVHN2Y2VUc2hhMVRtdXNyVGNkYXRUbWRhdFRwZG1uVGFncnBVY2xhc3NEYXNkZlt0ZXN0YWNjb3VudBAAUE8QFF7OzuEEGWTTwzzSp/rjY6ubHW2rQDNBv7zNQtQUQFJja18QF2NvbS5hcHBsZS5zZWN1cml0eS5ja2tzVGdlbnAIHyYrMDU6P0RJTlNZXmpsbYSFjpGrAAAAAAAAAQEAAAAAAAAAFgAAAAAAAAAAAAAAAAAAALA=" options:0];
1049 NSMutableDictionary * item = [[NSPropertyListSerialization propertyListWithData:itemdata
1050 options:0
1051 format:nil
1052 error:&error] mutableCopy];
1053 XCTAssertNil(error, "Error should be nil parsing base64 item");
1054
1055 item[@"v_Data"] = [@"conflictingdata" dataUsingEncoding:NSUTF8StringEncoding];
1056 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
1057 CKRecord* mismatchedRecord = [self newRecord:ckrid withNewItemData:item];
1058 [self.keychainZone addToZone: mismatchedRecord];
1059
1060 self.keychainView.holdIncomingQueueOperation = [CKKSResultOperation named:@"hold-incoming" withBlock:^{
1061 secnotice("ckks", "Releasing process incoming queue hold");
1062 }];
1063
1064 NSData* firstItemData = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding];
1065
1066 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1067 NSDictionary* result = [self pcsAddItem:account
1068 data:firstItemData
1069 serviceIdentifier:(NSNumber*)servIdentifier
1070 publicKey:(NSData*)publicKey
1071 publicIdentity:(NSData*)publicIdentity
1072 expectingSync:false];
1073 XCTAssertNotNil(result, "Should receive result from adding item");
1074
1075 NSData* persistentRef = result[(id)kSecValuePersistentRef];
1076 NSData* sha1 = result[(id)kSecAttrSHA1];
1077
1078 // Ensure that fetching the item without grabbing data returns the same SHA1
1079 NSDictionary* prefquery = @{(id)kSecClass : (id)kSecClassGenericPassword,
1080 (id)kSecReturnAttributes : @YES,
1081 (id)kSecAttrSynchronizable : @YES,
1082 (id)kSecAttrPersistentReference : persistentRef,
1083 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1084 };
1085 CFTypeRef prefresult = NULL;
1086 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef)prefquery, &prefresult), "Should be able to find item by persistent ref");
1087 NSDictionary* newPersistentRefResult = (NSDictionary*) CFBridgingRelease(prefresult);
1088 prefresult = NULL;
1089 XCTAssertNotNil(newPersistentRefResult, "Should have received item attributes");
1090 XCTAssertEqualObjects(newPersistentRefResult[(id)kSecAttrSHA1], sha1, "SHA1 should match between Add and Find (with data)");
1091 XCTAssertNil(newPersistentRefResult[(id)kSecValueData], "Should have returned no data");
1092
1093 // Ensure that fetching the item and grabbing data returns the same SHA1
1094 prefquery = @{(id)kSecClass : (id)kSecClassGenericPassword,
1095 (id)kSecReturnAttributes : @YES,
1096 (id)kSecReturnData : @YES,
1097 (id)kSecAttrSynchronizable : @YES,
1098 (id)kSecAttrPersistentReference : persistentRef,
1099 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1100 };
1101 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef)prefquery, &prefresult), "Should be able to find item by persistent ref");
1102 newPersistentRefResult = (NSDictionary*) CFBridgingRelease(prefresult);
1103 XCTAssertNotNil(newPersistentRefResult, "Should have received item attributes");
1104 XCTAssertEqualObjects(newPersistentRefResult[(id)kSecAttrSHA1], sha1, "SHA1 should match between Add and Find (with data)");
1105 XCTAssertEqualObjects(newPersistentRefResult[(id)kSecValueData], firstItemData, "Should have returned data matching the item we put in");
1106
1107 // Set the current pointer to the result of adding this item. This should fail.
1108 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs before incoming queue operation"];
1109 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
1110 (__bridge CFStringRef)@"pcsservice",
1111 (__bridge CFStringRef)@"keychain",
1112 (__bridge CFDataRef)persistentRef,
1113 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
1114 XCTAssertNotNil((__bridge NSError*)cferror, "Should error setting current item to hash of item which failed to sync (before incoming queue operation)");
1115 [setCurrentExpectation fulfill];
1116 });
1117
1118 [self waitForExpectations:@[setCurrentExpectation] timeout:20];
1119
1120 // Now, release the incoming queue processing and retry the failure
1121 [self.operationQueue addOperation:self.keychainView.holdIncomingQueueOperation];
1122 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
1123
1124 setCurrentExpectation = [self expectationWithDescription: @"callback occurs after incoming queue operation"];
1125 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
1126 (__bridge CFStringRef)@"pcsservice",
1127 (__bridge CFStringRef)@"keychain",
1128 (__bridge CFDataRef)persistentRef,
1129 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
1130 XCTAssertNotNil((__bridge NSError*)cferror, "Should error setting current item to hash of item which failed to sync (after incoming queue operation)");
1131 [setCurrentExpectation fulfill];
1132 });
1133
1134 [self waitForExpectations:@[setCurrentExpectation] timeout:20];
1135
1136 // Reissue a fetch and find the new persistent ref and sha1 for the item at this UUID
1137 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1138
1139 // The conflicting item update should have won
1140 [self checkGenericPassword:@"conflictingdata" account:account];
1141
1142 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
1143 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1144 (id)kSecAttrAccount : account,
1145 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1146 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1147 (id)kSecReturnAttributes: @YES,
1148 (id)kSecReturnPersistentRef: @YES,
1149 };
1150
1151 CFTypeRef cfresult = NULL;
1152 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Finding item %@", account);
1153 NSDictionary* newResult = CFBridgingRelease(cfresult);
1154 XCTAssertNotNil(newResult, "Received an item");
1155
1156 NSData* newPersistentRef = newResult[(id)kSecValuePersistentRef];
1157 NSData* newSha1 = newResult[(id)kSecAttrSHA1];
1158
1159 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
1160 deletedRecordTypeCounts:nil
1161 zoneID:self.keychainZoneID
1162 checkModifiedRecord:nil
1163 runAfterModification:nil];
1164
1165 XCTestExpectation* newSetCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
1166 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
1167 (__bridge CFStringRef)@"pcsservice",
1168 (__bridge CFStringRef)@"keychain",
1169 (__bridge CFDataRef)newPersistentRef,
1170 (__bridge CFDataRef)newSha1, NULL, NULL, ^ (CFErrorRef cferror) {
1171 XCTAssertNil((__bridge NSError*)cferror, "Shouldn't error setting current item");
1172 [newSetCurrentExpectation fulfill];
1173 });
1174
1175 [self waitForExpectations:@[newSetCurrentExpectation] timeout:20];
1176
1177 SecResetLocalSecuritydXPCFakeEntitlements();
1178 }
1179
1180 @end
1181
1182 #endif // OCTAGON