]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTests+CurrentPointerAPI.m
Security-59306.11.20.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
394 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
395 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
396
397 self.silentFetchesAllowed = false;
398 [self startCKKSSubsystem];
399
400 // Ensure there's no current pointer
401 [self fetchCurrentPointerExpectingError:false];
402
403 // Should NOT add an item to CloudKit
404 NSDictionary* result = [self pcsAddItem:@"testaccount"
405 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
406 serviceIdentifier:(NSNumber*)servIdentifier
407 publicKey:(NSData*)publicKey
408 publicIdentity:(NSData*)publicIdentity
409 expectingSync:false];
410 XCTAssertNotNil(result, "Received result from adding item");
411
412 NSData* persistentRef = result[(id)kSecValuePersistentRef];
413 NSData* sha1 = result[(id)kSecAttrSHA1];
414
415 // Set the 'current' pointer. This should fail.
416 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
417
418 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
419 (__bridge CFStringRef)@"pcsservice",
420 (__bridge CFStringRef)@"keychain",
421 (__bridge CFDataRef)persistentRef,
422 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
423 NSError* error = (__bridge NSError*)cferror;
424 XCTAssertNotNil(error, "Error setting current item with no CloudKit account");
425 [setCurrentExpectation fulfill];
426 });
427
428 [self waitForExpectationsWithTimeout:8.0 handler:nil];
429 SecResetLocalSecuritydXPCFakeEntitlements();
430 }
431
432 - (void)testPCSCurrentPointerAddNonSyncItem {
433 SecResetLocalSecuritydXPCFakeEntitlements();
434 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
435 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
436 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
437
438 NSNumber* servIdentifier = @3;
439 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
440 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
441
442 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
443 [self startCKKSSubsystem];
444 [self.keychainView waitForKeyHierarchyReadiness];
445
446 NSMutableDictionary* query = [self pcsAddItemQuery:@"account"
447 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
448 serviceIdentifier:servIdentifier
449 publicKey:publicKey
450 publicIdentity:publicIdentity];
451 query[(id)kSecAttrSynchronizable] = @NO;
452
453 CFTypeRef cfresult = NULL;
454 XCTestExpectation* syncExpectation = [self expectationWithDescription: @"_SecItemAddAndNotifyOnSync callback occured"];
455
456 // Note that you will NOT receive a notification here, since you're adding a nonsync item
457 syncExpectation.inverted = YES;
458
459 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, &cfresult, ^(bool didSync, CFErrorRef error) {
460 XCTAssertFalse(didSync, "Item did not sync");
461 XCTAssertNotNil((__bridge NSError*)error, "Error syncing item");
462
463 [syncExpectation fulfill];
464 }), @"_SecItemAddAndNotifyOnSync succeeded");
465
466 // We don't expect this callback to fire, so give it a second or so
467 [self waitForExpectations:@[syncExpectation] timeout:20];
468
469 NSDictionary* result = CFBridgingRelease(cfresult);
470
471 XCTAssertNotNil(result, "Received result from adding item");
472
473 NSData* persistentRef = result[(id)kSecValuePersistentRef];
474 NSData* sha1 = result[(id)kSecAttrSHA1];
475
476 // Set the 'current' pointer. This should fail.
477 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
478
479 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
480 (__bridge CFStringRef)@"pcsservice",
481 (__bridge CFStringRef)@"keychain",
482 (__bridge CFDataRef)persistentRef,
483 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
484 NSError* error = (__bridge NSError*)cferror;
485 XCTAssertNotNil(error, "Error setting current item to nonsyncable item");
486 [setCurrentExpectation fulfill];
487 });
488
489 [self waitForExpectations:@[setCurrentExpectation] timeout:20];
490 SecResetLocalSecuritydXPCFakeEntitlements();
491 }
492
493 - (void)testPCSCurrentPointerReceive {
494 SecResetLocalSecuritydXPCFakeEntitlements();
495 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
496 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
497 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
498
499 NSNumber* servIdentifier = @3;
500 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
501 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
502
503 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
504 [self startCKKSSubsystem];
505
506 // Let things shake themselves out.
507 [self.keychainView waitForKeyHierarchyReadiness];
508
509 // Ensure there's no current pointer
510 [self fetchCurrentPointerExpectingError:false];
511
512 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
513 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
514 PCSServiceIdentifier:(NSNumber *)servIdentifier
515 PCSPublicKey:publicKey
516 PCSPublicIdentity:publicIdentity]];
517
518 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
519
520 NSDictionary* result = [self pcsAddItem:@"testaccount"
521 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
522 serviceIdentifier:(NSNumber*)servIdentifier
523 publicKey:(NSData*)publicKey
524 publicIdentity:(NSData*)publicIdentity
525 expectingSync:true];
526 XCTAssertNotNil(result, "Received result from adding item");
527 NSData* persistentRef = result[(id)kSecValuePersistentRef];
528
529 [self waitForExpectations:@[keychainChanged] timeout:8];
530
531 // And a second item
532 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
533 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
534 PCSServiceIdentifier:(NSNumber *)servIdentifier
535 PCSPublicKey:publicKey
536 PCSPublicIdentity:publicIdentity]];
537 result = [self pcsAddItem:@"testaccount2"
538 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
539 serviceIdentifier:(NSNumber*)servIdentifier
540 publicKey:(NSData*)publicKey
541 publicIdentity:(NSData*)publicIdentity
542 expectingSync:true];
543 XCTAssertNotNil(result, "Received result from adding item");
544 NSData* persistentRef2 = result[(id)kSecValuePersistentRef];
545
546 // Check that the records are where we expect them in CloudKit
547 [self waitForCKModifications];
548 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
549 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
550 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
551
552 CKRecordID* pcsItemRecordID2 = [[CKRecordID alloc] initWithRecordName: @"3AB8E78D-75AF-CFEF-F833-FA3E3E90978A" zoneID:self.keychainZoneID];
553 CKRecord* record2 = self.keychainZone.currentDatabase[pcsItemRecordID2];
554 XCTAssertNotNil(record2, "Found 2nd record in CloudKit at expected UUID");
555
556 // Still no current pointer.
557 [self fetchCurrentPointerExpectingError:false];
558
559 // Another machine comes along and updates the pointer!
560 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
561 currentItemUUID:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3"
562 state:SecCKKSProcessedStateRemote
563 zoneID:self.keychainZoneID
564 encodedCKRecord:nil];
565 [self.keychainZone addToZone: [cip CKRecordWithZoneID:self.keychainZoneID]];
566 CKRecordID* currentPointerRecordID = [[CKRecordID alloc] initWithRecordName: @"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID];
567 CKRecord* currentPointerRecord = self.keychainZone.currentDatabase[currentPointerRecordID];
568 XCTAssertNotNil(currentPointerRecord, "Found record in CloudKit at expected UUID");
569
570 // Ensure that receiving the current item pointer generates a notification
571 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
572
573 [self.keychainView notifyZoneChange:nil];
574 [self.keychainView waitForFetchAndIncomingQueueProcessing];
575
576 [self waitForExpectations:@[keychainChanged] timeout:8];
577 [self fetchCurrentPointer:false persistentRef:persistentRef];
578
579 // And again!
580 CKKSCurrentItemPointer* cip2 = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
581 currentItemUUID:pcsItemRecordID2.recordName
582 state:SecCKKSProcessedStateRemote
583 zoneID:self.keychainZoneID
584 encodedCKRecord:nil];
585 [self.keychainZone addToZone: [cip2 CKRecordWithZoneID:self.keychainZoneID]];
586 CKRecordID* currentPointerRecordID2 = [[CKRecordID alloc] initWithRecordName: @"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID];
587 CKRecord* currentPointerRecord2 = self.keychainZone.currentDatabase[currentPointerRecordID2];
588 XCTAssertNotNil(currentPointerRecord2, "Found record in CloudKit at expected UUID");
589
590 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
591
592 [self.keychainView notifyZoneChange:nil];
593 [self.keychainView waitForFetchAndIncomingQueueProcessing];
594
595 [self waitForExpectations:@[keychainChanged] timeout:8];
596 [self fetchCurrentPointer:false persistentRef:persistentRef2];
597
598 SecResetLocalSecuritydXPCFakeEntitlements();
599 }
600
601
602 - (void)testPCSCurrentPointerReceiveDelete {
603 SecResetLocalSecuritydXPCFakeEntitlements();
604 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
605 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
606 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
607
608 NSNumber* servIdentifier = @3;
609 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
610 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
611
612 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
613 [self startCKKSSubsystem];
614
615 // Let things shake themselves out.
616 [self.keychainView waitForKeyHierarchyReadiness];
617
618 // Ensure there's no current pointer
619 [self fetchCurrentPointerExpectingError:false];
620
621 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
622 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
623 PCSServiceIdentifier:(NSNumber *)servIdentifier
624 PCSPublicKey:publicKey
625 PCSPublicIdentity:publicIdentity]];
626
627 NSDictionary* result = [self pcsAddItem:@"testaccount"
628 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
629 serviceIdentifier:(NSNumber*)servIdentifier
630 publicKey:(NSData*)publicKey
631 publicIdentity:(NSData*)publicIdentity
632 expectingSync:true];
633 XCTAssertNotNil(result, "Received result from adding item");
634 NSData* persistentRef = result[(id)kSecValuePersistentRef];
635
636 // Check that the record is where we expect it in CloudKit
637 [self waitForCKModifications];
638 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
639 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
640 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
641
642 // Still no current pointer.
643 [self fetchCurrentPointerExpectingError:false];
644
645 // Another machine comes along and updates the pointer!
646 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
647 currentItemUUID:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3"
648 state:SecCKKSProcessedStateRemote
649 zoneID:self.keychainZoneID
650 encodedCKRecord:nil];
651 [self.keychainZone addToZone: [cip CKRecordWithZoneID:self.keychainZoneID]];
652 CKRecordID* currentPointerRecordID = [[CKRecordID alloc] initWithRecordName: @"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID];
653 CKRecord* currentPointerRecord = self.keychainZone.currentDatabase[currentPointerRecordID];
654 XCTAssertNotNil(currentPointerRecord, "Found record in CloudKit at expected UUID");
655
656 [self.keychainView notifyZoneChange:nil];
657 [self.keychainView waitForFetchAndIncomingQueueProcessing];
658
659 [self fetchCurrentPointer:false persistentRef:persistentRef];
660
661 // Ensure that receiving the current item pointer generates a notification
662 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
663
664 // Another machine comes along and deletes the pointer!
665 [self.keychainZone deleteCKRecordIDFromZone: currentPointerRecordID];
666 [self.keychainView notifyZoneChange:nil];
667 [self.keychainView waitForFetchAndIncomingQueueProcessing];
668 [self waitForExpectations:@[keychainChanged] timeout:8];
669
670 [self fetchCurrentPointerExpectingError:false];
671
672 SecResetLocalSecuritydXPCFakeEntitlements();
673 }
674
675
676 - (void)testPCSCurrentPointerRecoverFromRecordExistsError {
677 SecResetLocalSecuritydXPCFakeEntitlements();
678 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
679 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
680 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
681
682 NSNumber* servIdentifier = @3;
683 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
684 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
685
686 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
687 [self startCKKSSubsystem];
688
689 // Let things shake themselves out.
690 [self.keychainView waitForKeyHierarchyReadiness];
691
692 // Ensure there's no current pointer
693 [self fetchCurrentPointerExpectingError:false];
694
695 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
696 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
697 PCSServiceIdentifier:(NSNumber *)servIdentifier
698 PCSPublicKey:publicKey
699 PCSPublicIdentity:publicIdentity]];
700
701 NSDictionary* result = [self pcsAddItem:@"testaccount"
702 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
703 serviceIdentifier:(NSNumber*)servIdentifier
704 publicKey:(NSData*)publicKey
705 publicIdentity:(NSData*)publicIdentity
706 expectingSync:true];
707 XCTAssertNotNil(result, "Received result from adding item");
708
709 // Check that the record is where we expect it in CloudKit
710 [self waitForCKModifications];
711 NSString* recordUUID = @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3";
712 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName:recordUUID zoneID:self.keychainZoneID];
713 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
714 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
715
716 // Someone else sets the current record pointer
717 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
718 currentItemUUID:recordUUID
719 state:SecCKKSProcessedStateRemote
720 zoneID:self.keychainZoneID
721 encodedCKRecord:nil];
722 XCTAssertNotNil(cip, "Should have created a CIP");
723 CKRecord* cipRecord = [cip CKRecordWithZoneID:self.keychainZoneID];
724 XCTAssertNotNil(cipRecord, "Should have created a CKRecord for this CIP");
725 [self.keychainZone addToZone: cipRecord];
726
727 NSData* persistentRef = result[(id)kSecValuePersistentRef];
728 NSData* sha1 = result[(id)kSecAttrSHA1];
729
730 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
731
732 // Set the 'current' pointer.
733 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
734
735 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
736 (__bridge CFStringRef)@"pcsservice",
737 (__bridge CFStringRef)@"keychain",
738 (__bridge CFDataRef)persistentRef,
739 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
740 NSError* error = (__bridge NSError*)cferror;
741 XCTAssertNotNil(error, "Should have received an error setting current item (because of conflict)");
742 [setCurrentExpectation fulfill];
743 });
744 OCMVerifyAllWithDelay(self.mockDatabase, 20);
745 [self waitForCKModifications];
746 [self waitForExpectationsWithTimeout:8.0 handler:nil];
747
748 [self.keychainView waitUntilAllOperationsAreFinished];
749
750 [self fetchCurrentPointer:false persistentRef:persistentRef];
751
752 SecResetLocalSecuritydXPCFakeEntitlements();
753 }
754
755 - (void)testPCSCurrentPointerWasCurrent {
756 SecResetLocalSecuritydXPCFakeEntitlements();
757 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
758 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
759 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
760
761 NSNumber* servIdentifier = @3;
762 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
763 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
764
765 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
766 [self startCKKSSubsystem];
767
768 // Let things shake themselves out.
769 [self.keychainView waitForKeyHierarchyReadiness];
770
771 // Ensure there's no current pointer
772 [self fetchCurrentPointerExpectingError:false];
773
774 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
775 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
776 PCSServiceIdentifier:(NSNumber *)servIdentifier
777 PCSPublicKey:publicKey
778 PCSPublicIdentity:publicIdentity]];
779
780 NSDictionary* result = [self pcsAddItem:@"testaccount"
781 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
782 serviceIdentifier:(NSNumber*)servIdentifier
783 publicKey:(NSData*)publicKey
784 publicIdentity:(NSData*)publicIdentity
785 expectingSync:true];
786 XCTAssertNotNil(result, "Received result from adding item");
787
788 // Check that the record is where we expect it in CloudKit
789 [self waitForCKModifications];
790 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
791 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
792 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
793
794 NSData* persistentRef = result[(id)kSecValuePersistentRef];
795 NSData* sha1 = result[(id)kSecAttrSHA1];
796
797 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
798 deletedRecordTypeCounts:nil
799 zoneID:self.keychainZoneID
800 checkModifiedRecord:nil
801 runAfterModification:nil];
802
803 // Set the 'current' pointer.
804 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
805
806 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
807 (__bridge CFStringRef)@"pcsservice",
808 (__bridge CFStringRef)@"keychain",
809 (__bridge CFDataRef)persistentRef,
810 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
811 NSError* error = (__bridge NSError*)cferror;
812 XCTAssertNil(error, "No error setting current item");
813 [setCurrentExpectation fulfill];
814 });
815 OCMVerifyAllWithDelay(self.mockDatabase, 20);
816 [self waitForExpectationsWithTimeout:8.0 handler:nil];
817 [self waitForCKModifications];
818
819 // Set the 'was current' flag on the record
820 CKRecord* modifiedRecord = [record copy];
821 modifiedRecord[SecCKRecordServerWasCurrent] = [NSNumber numberWithInteger:10];
822 [self.keychainZone addToZone:modifiedRecord];
823
824 [self.keychainView notifyZoneChange:nil];
825 [self.keychainView waitForFetchAndIncomingQueueProcessing];
826
827 // Check that the number is on the CKKSMirrorEntry
828 [self.keychainView dispatchSync: ^bool {
829 NSError* error = nil;
830 CKKSMirrorEntry* ckme = [CKKSMirrorEntry fromDatabase:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID error:&error];
831
832 XCTAssertNil(error, "no error fetching ckme");
833 XCTAssertNotNil(ckme, "Received a ckme");
834
835 XCTAssertEqual(ckme.wasCurrent, 10u, "Properly received wasCurrent");
836
837 return true;
838 }];
839 }
840
841 -(void)testPCSCurrentPointerWriteFailure {
842 SecResetLocalSecuritydXPCFakeEntitlements();
843 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
844 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
845 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
846
847 NSNumber* servIdentifier = @3;
848 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
849 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
850
851 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
852 [self startCKKSSubsystem];
853
854 // Let things shake themselves out.
855 [self.keychainView waitForKeyHierarchyReadiness];
856
857 // Ensure there's no current pointer
858 [self fetchCurrentPointerExpectingError:false];
859
860 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
861 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
862 PCSServiceIdentifier:(NSNumber *)servIdentifier
863 PCSPublicKey:publicKey
864 PCSPublicIdentity:publicIdentity]];
865
866 NSDictionary* result = [self pcsAddItem:@"testaccount"
867 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
868 serviceIdentifier:(NSNumber*)servIdentifier
869 publicKey:(NSData*)publicKey
870 publicIdentity:(NSData*)publicIdentity
871 expectingSync:true];
872 XCTAssertNotNil(result, "Received result from adding item");
873
874 // Check that the record is where we expect it in CloudKit
875 [self waitForCKModifications];
876 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
877 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
878 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
879
880 NSData* persistentRef = result[(id)kSecValuePersistentRef];
881 NSData* sha1 = result[(id)kSecAttrSHA1];
882
883 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
884
885 // Set the 'current' pointer.
886 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
887
888 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
889 (__bridge CFStringRef)@"pcsservice",
890 (__bridge CFStringRef)@"keychain",
891 (__bridge CFDataRef)persistentRef,
892 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
893 NSError* error = (__bridge NSError*)cferror;
894 XCTAssertNotNil(error, "Error setting current item when the write fails");
895 [setCurrentExpectation fulfill];
896 });
897 OCMVerifyAllWithDelay(self.mockDatabase, 40);
898
899 [self waitForExpectationsWithTimeout:8.0 handler:nil];
900
901 SecResetLocalSecuritydXPCFakeEntitlements();
902 }
903
904 - (void)testPCSCurrentRecoverFromDanglingPointer {
905 SecResetLocalSecuritydXPCFakeEntitlements();
906 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
907 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
908 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
909
910 NSNumber* servIdentifier = @3;
911 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
912 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
913
914 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
915 [self startCKKSSubsystem];
916
917 // Let things shake themselves out.
918 [self.keychainView waitForKeyHierarchyReadiness];
919
920 // Ensure there's no current pointer
921 [self fetchCurrentPointerExpectingError:false];
922
923 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
924 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
925 PCSServiceIdentifier:(NSNumber *)servIdentifier
926 PCSPublicKey:publicKey
927 PCSPublicIdentity:publicIdentity]];
928
929 NSDictionary* result = [self pcsAddItem:@"testaccount"
930 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
931 serviceIdentifier:(NSNumber*)servIdentifier
932 publicKey:(NSData*)publicKey
933 publicIdentity:(NSData*)publicIdentity
934 expectingSync:true];
935 XCTAssertNotNil(result, "Received result from adding item");
936
937 // Check that the record is where we expect it in CloudKit
938 [self waitForCKModifications];
939 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
940 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
941 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
942
943 NSData* persistentRef = result[(id)kSecValuePersistentRef];
944 NSData* sha1 = result[(id)kSecAttrSHA1];
945
946 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
947 deletedRecordTypeCounts:nil
948 zoneID:self.keychainZoneID
949 checkModifiedRecord:nil
950 runAfterModification:nil];
951
952 // Set the 'current' pointer.
953 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
954
955 // Ensure that setting the current pointer sends a notification
956 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
957 (__bridge CFStringRef)@"pcsservice",
958 (__bridge CFStringRef)@"keychain",
959 (__bridge CFDataRef)persistentRef,
960 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
961 NSError* error = (__bridge NSError*)cferror;
962 XCTAssertNil(error, "No error setting current item");
963 [setCurrentExpectation fulfill];
964 });
965 OCMVerifyAllWithDelay(self.mockDatabase, 20);
966 [self waitForCKModifications];
967
968 [self waitForExpectationsWithTimeout:8.0 handler:nil];
969
970 // Delete the keychain item
971 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
972 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
973 (id)kSecClass : (id)kSecClassGenericPassword,
974 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
975 (id)kSecAttrAccount:@"testaccount",
976 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
977 }), "Should receive no error deleting item");
978 OCMVerifyAllWithDelay(self.mockDatabase, 20);
979
980 // Now, fetch the current pointer: we should get an error
981 [self fetchCurrentPointerExpectingError:false];
982
983 // Setting the current item pointer again, using a NULL old value, should work.
984 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
985 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
986 PCSServiceIdentifier:(NSNumber *)servIdentifier
987 PCSPublicKey:publicKey
988 PCSPublicIdentity:publicIdentity]];
989
990 result = [self pcsAddItem:@"testaccount2"
991 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
992 serviceIdentifier:(NSNumber*)servIdentifier
993 publicKey:(NSData*)publicKey
994 publicIdentity:(NSData*)publicIdentity
995 expectingSync:true];
996 XCTAssertNotNil(result, "Should have result from adding item2");
997
998 persistentRef = result[(id)kSecValuePersistentRef];
999 sha1 = result[(id)kSecAttrSHA1];
1000
1001 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
1002 deletedRecordTypeCounts:nil
1003 zoneID:self.keychainZoneID
1004 checkModifiedRecord:nil
1005 runAfterModification:nil];
1006
1007 // Set the 'current' pointer.
1008 setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
1009
1010 // Ensure that setting the current pointer sends a notification
1011 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
1012 (__bridge CFStringRef)@"pcsservice",
1013 (__bridge CFStringRef)@"keychain",
1014 (__bridge CFDataRef)persistentRef,
1015 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
1016 NSError* error = (__bridge NSError*)cferror;
1017 XCTAssertNil(error, "No error setting current item");
1018 [setCurrentExpectation fulfill];
1019 });
1020 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1021 [self waitForCKModifications];
1022 [self waitForExpectationsWithTimeout:8.0 handler:nil];
1023
1024 SecResetLocalSecuritydXPCFakeEntitlements();
1025 }
1026
1027 -(void)testPCSCurrentSetConflictedItemAsCurrent {
1028 SecResetLocalSecuritydXPCFakeEntitlements();
1029 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
1030 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
1031 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
1032
1033 NSNumber* servIdentifier = @3;
1034 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
1035 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
1036
1037 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1038 [self startCKKSSubsystem];
1039
1040 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1041 [self.keychainView waitUntilAllOperationsAreFinished];
1042
1043 // Before CKKS can add the item, shove a conflicting one into CloudKit
1044 NSError* error = nil;
1045
1046 NSString* account = @"testaccount";
1047
1048 // Create an item in CloudKit that will conflict in both UUID and primary key
1049 NSData* itemdata = [[NSData alloc] initWithBase64EncodedString:@"YnBsaXN0MDDbAQIDBAUGBwgJCgsMDQ4PEBESEhMUFVZ2X0RhdGFUYWNjdFR0b21iVHN2Y2VUc2hhMVRtdXNyVGNkYXRUbWRhdFRwZG1uVGFncnBVY2xhc3NEYXNkZlt0ZXN0YWNjb3VudBAAUE8QFF7OzuEEGWTTwzzSp/rjY6ubHW2rQDNBv7zNQtQUQFJja18QF2NvbS5hcHBsZS5zZWN1cml0eS5ja2tzVGdlbnAIHyYrMDU6P0RJTlNZXmpsbYSFjpGrAAAAAAAAAQEAAAAAAAAAFgAAAAAAAAAAAAAAAAAAALA=" options:0];
1050 NSMutableDictionary * item = [[NSPropertyListSerialization propertyListWithData:itemdata
1051 options:0
1052 format:nil
1053 error:&error] mutableCopy];
1054 XCTAssertNil(error, "Error should be nil parsing base64 item");
1055
1056 item[@"v_Data"] = [@"conflictingdata" dataUsingEncoding:NSUTF8StringEncoding];
1057 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
1058 CKRecord* mismatchedRecord = [self newRecord:ckrid withNewItemData:item];
1059 [self.keychainZone addToZone: mismatchedRecord];
1060
1061 self.keychainView.holdIncomingQueueOperation = [CKKSResultOperation named:@"hold-incoming" withBlock:^{
1062 secnotice("ckks", "Releasing process incoming queue hold");
1063 }];
1064
1065 NSData* firstItemData = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding];
1066
1067 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1068 NSDictionary* result = [self pcsAddItem:account
1069 data:firstItemData
1070 serviceIdentifier:(NSNumber*)servIdentifier
1071 publicKey:(NSData*)publicKey
1072 publicIdentity:(NSData*)publicIdentity
1073 expectingSync:false];
1074 XCTAssertNotNil(result, "Should receive result from adding item");
1075
1076 NSData* persistentRef = result[(id)kSecValuePersistentRef];
1077 NSData* sha1 = result[(id)kSecAttrSHA1];
1078
1079 // Ensure that fetching the item without grabbing data returns the same SHA1
1080 NSDictionary* prefquery = @{(id)kSecClass : (id)kSecClassGenericPassword,
1081 (id)kSecReturnAttributes : @YES,
1082 (id)kSecAttrSynchronizable : @YES,
1083 (id)kSecAttrPersistentReference : persistentRef,
1084 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1085 };
1086 CFTypeRef prefresult = NULL;
1087 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef)prefquery, &prefresult), "Should be able to find item by persistent ref");
1088 NSDictionary* newPersistentRefResult = (NSDictionary*) CFBridgingRelease(prefresult);
1089 prefresult = NULL;
1090 XCTAssertNotNil(newPersistentRefResult, "Should have received item attributes");
1091 XCTAssertEqualObjects(newPersistentRefResult[(id)kSecAttrSHA1], sha1, "SHA1 should match between Add and Find (with data)");
1092 XCTAssertNil(newPersistentRefResult[(id)kSecValueData], "Should have returned no data");
1093
1094 // Ensure that fetching the item and grabbing data returns the same SHA1
1095 prefquery = @{(id)kSecClass : (id)kSecClassGenericPassword,
1096 (id)kSecReturnAttributes : @YES,
1097 (id)kSecReturnData : @YES,
1098 (id)kSecAttrSynchronizable : @YES,
1099 (id)kSecAttrPersistentReference : persistentRef,
1100 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1101 };
1102 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef)prefquery, &prefresult), "Should be able to find item by persistent ref");
1103 newPersistentRefResult = (NSDictionary*) CFBridgingRelease(prefresult);
1104 XCTAssertNotNil(newPersistentRefResult, "Should have received item attributes");
1105 XCTAssertEqualObjects(newPersistentRefResult[(id)kSecAttrSHA1], sha1, "SHA1 should match between Add and Find (with data)");
1106 XCTAssertEqualObjects(newPersistentRefResult[(id)kSecValueData], firstItemData, "Should have returned data matching the item we put in");
1107
1108 // Set the current pointer to the result of adding this item. This should fail.
1109 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs before incoming queue operation"];
1110 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
1111 (__bridge CFStringRef)@"pcsservice",
1112 (__bridge CFStringRef)@"keychain",
1113 (__bridge CFDataRef)persistentRef,
1114 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
1115 XCTAssertNotNil((__bridge NSError*)cferror, "Should error setting current item to hash of item which failed to sync (before incoming queue operation)");
1116 [setCurrentExpectation fulfill];
1117 });
1118
1119 [self waitForExpectations:@[setCurrentExpectation] timeout:20];
1120
1121 // Now, release the incoming queue processing and retry the failure
1122 [self.operationQueue addOperation:self.keychainView.holdIncomingQueueOperation];
1123 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
1124
1125 setCurrentExpectation = [self expectationWithDescription: @"callback occurs after incoming queue operation"];
1126 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
1127 (__bridge CFStringRef)@"pcsservice",
1128 (__bridge CFStringRef)@"keychain",
1129 (__bridge CFDataRef)persistentRef,
1130 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
1131 XCTAssertNotNil((__bridge NSError*)cferror, "Should error setting current item to hash of item which failed to sync (after incoming queue operation)");
1132 [setCurrentExpectation fulfill];
1133 });
1134
1135 [self waitForExpectations:@[setCurrentExpectation] timeout:20];
1136
1137 // Reissue a fetch and find the new persistent ref and sha1 for the item at this UUID
1138 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1139
1140 // The conflicting item update should have won
1141 [self checkGenericPassword:@"conflictingdata" account:account];
1142
1143 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
1144 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1145 (id)kSecAttrAccount : account,
1146 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1147 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1148 (id)kSecReturnAttributes: @YES,
1149 (id)kSecReturnPersistentRef: @YES,
1150 };
1151
1152 CFTypeRef cfresult = NULL;
1153 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Finding item %@", account);
1154 NSDictionary* newResult = CFBridgingRelease(cfresult);
1155 XCTAssertNotNil(newResult, "Received an item");
1156
1157 NSData* newPersistentRef = newResult[(id)kSecValuePersistentRef];
1158 NSData* newSha1 = newResult[(id)kSecAttrSHA1];
1159
1160 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
1161 deletedRecordTypeCounts:nil
1162 zoneID:self.keychainZoneID
1163 checkModifiedRecord:nil
1164 runAfterModification:nil];
1165
1166 XCTestExpectation* newSetCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
1167 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
1168 (__bridge CFStringRef)@"pcsservice",
1169 (__bridge CFStringRef)@"keychain",
1170 (__bridge CFDataRef)newPersistentRef,
1171 (__bridge CFDataRef)newSha1, NULL, NULL, ^ (CFErrorRef cferror) {
1172 XCTAssertNil((__bridge NSError*)cferror, "Shouldn't error setting current item");
1173 [newSetCurrentExpectation fulfill];
1174 });
1175
1176 [self waitForExpectations:@[newSetCurrentExpectation] timeout:20];
1177
1178 SecResetLocalSecuritydXPCFakeEntitlements();
1179 }
1180
1181 @end
1182
1183 #endif // OCTAGON