4 #import <CloudKit/CloudKit.h>
5 #import <XCTest/XCTest.h>
6 #import <OCMock/OCMock.h>
9 #include <Security/SecItemPriv.h>
10 #import <TrustedPeers/TrustedPeers.h>
11 #import <TrustedPeers/TPPBPolicyKeyViewMapping.h>
12 #import <TrustedPeers/TPDictionaryMatchingRules.h>
14 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
15 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
16 #import "keychain/ckks/CKKS.h"
17 #import "keychain/ckks/CKKSKeychainView.h"
18 #import "keychain/ckks/CKKSCurrentKeyPointer.h"
19 #import "keychain/ckks/CKKSItemEncrypter.h"
20 #import "keychain/ckks/CKKSKey.h"
21 #import "keychain/ckks/CKKSOutgoingQueueEntry.h"
22 #import "keychain/ckks/CKKSIncomingQueueEntry.h"
23 #import "keychain/ckks/CKKSStates.h"
24 #import "keychain/ckks/CKKSSynchronizeOperation.h"
25 #import "keychain/ckks/CKKSViewManager.h"
26 #import "keychain/ckks/CKKSZoneStateEntry.h"
27 #import "keychain/ckks/CKKSManifest.h"
29 #import "keychain/ckks/tests/CKKSTests+MultiZone.h"
30 #import "keychain/ckks/tests/MockCloudKit.h"
32 #pragma clang diagnostic push
33 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
34 #include <Security/SecureObjectSync/SOSCloudCircle.h>
35 #include "keychain/SecureObjectSync/SOSAccountPriv.h"
36 #include "keychain/SecureObjectSync/SOSAccount.h"
37 #include "keychain/SecureObjectSync/SOSInternal.h"
38 #include "keychain/SecureObjectSync/SOSFullPeerInfo.h"
39 #pragma clang diagnostic pop
41 #include <Security/SecKey.h>
42 #include <Security/SecKeyPriv.h>
43 #pragma clang diagnostic pop
45 @interface CloudKitKeychainSyncingMultiZoneTestsBase ()
48 @implementation CloudKitKeychainSyncingMultiZoneTestsBase
51 SecCKKSResetSyncing();
55 - (NSSet*)managedViewList {
56 NSMutableSet* parentSet = [[super managedViewList] mutableCopy];
57 [parentSet addObject:@"Passwords"];
61 // Make a policy as normal for most views, but Passwords is special
62 - (TPSyncingPolicy*)viewSortingPolicyForManagedViewList
64 NSMutableArray<TPPBPolicyKeyViewMapping*>* rules = [NSMutableArray array];
66 for(NSString* viewName in self.managedViewList) {
67 TPPBPolicyKeyViewMapping* mapping = [[TPPBPolicyKeyViewMapping alloc] init];
68 mapping.view = viewName;
70 // The real passwords view is on com.appple.cfnetwork, but for these tests, let's just use the sbd agrp (because of how the entitlements are specified)
71 if([viewName isEqualToString:@"Passwords"]) {
72 mapping.matchingRule = [TPDictionaryMatchingRule fieldMatch:@"agrp"
73 fieldRegex:[NSString stringWithFormat:@"^com\\.apple\\.sbd$"]];
75 mapping.matchingRule = [TPDictionaryMatchingRule fieldMatch:@"vwht"
76 fieldRegex:[NSString stringWithFormat:@"^%@$", viewName]];
79 [rules addObject:mapping];
82 NSSet<NSString*>* viewList = [self managedViewList];
83 TPSyncingPolicy* policy = [[TPSyncingPolicy alloc] initWithModel:@"test-policy"
84 version:[[TPPolicyVersion alloc] initWithVersion:1 hash:@"fake-policy-for-views"]
86 userControllableViews:[NSSet set]
87 syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_ENABLED
88 viewsToPiggybackTLKs:[viewList containsObject:@"Passwords"] ? [NSSet setWithObject:@"Passwords"] : [NSSet set]
89 keyViewMapping:rules];
95 SecCKKSSetSyncManifests(false);
96 SecCKKSSetEnforceManifests(false);
99 SecCKKSTestSetDisableSOS(false);
101 // Wait for the ViewManager to be brought up
102 XCTAssertEqual(0, [self.injectedManager.completedSecCKKSInitialize wait:20*NSEC_PER_SEC], "No timeout waiting for SecCKKSInitialize");
104 self.engramZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Engram" ownerName:CKCurrentUserDefaultName];
105 self.engramZone = [[FakeCKZone alloc] initZone: self.engramZoneID];
106 self.zones[self.engramZoneID] = self.engramZone;
107 self.engramView = [[CKKSViewManager manager] findOrCreateView:@"Engram"];
108 XCTAssertNotNil(self.engramView, "CKKSViewManager created the Engram view");
109 [self.ckksViews addObject:self.engramView];
110 [self.ckksZones addObject:self.engramZoneID];
112 self.manateeZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Manatee" ownerName:CKCurrentUserDefaultName];
113 self.manateeZone = [[FakeCKZone alloc] initZone: self.manateeZoneID];
114 self.zones[self.manateeZoneID] = self.manateeZone;
115 self.manateeView = [[CKKSViewManager manager] findOrCreateView:@"Manatee"];
116 XCTAssertNotNil(self.manateeView, "CKKSViewManager created the Manatee view");
117 [self.ckksViews addObject:self.manateeView];
118 [self.ckksZones addObject:self.manateeZoneID];
120 self.autoUnlockZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"AutoUnlock" ownerName:CKCurrentUserDefaultName];
121 self.autoUnlockZone = [[FakeCKZone alloc] initZone: self.autoUnlockZoneID];
122 self.zones[self.autoUnlockZoneID] = self.autoUnlockZone;
123 self.autoUnlockView = [[CKKSViewManager manager] findOrCreateView:@"AutoUnlock"];
124 XCTAssertNotNil(self.autoUnlockView, "CKKSViewManager created the AutoUnlock view");
125 [self.ckksViews addObject:self.autoUnlockView];
126 [self.ckksZones addObject:self.autoUnlockZoneID];
128 self.healthZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Health" ownerName:CKCurrentUserDefaultName];
129 self.healthZone = [[FakeCKZone alloc] initZone: self.healthZoneID];
130 self.zones[self.healthZoneID] = self.healthZone;
131 self.healthView = [[CKKSViewManager manager] findOrCreateView:@"Health"];
132 XCTAssertNotNil(self.healthView, "CKKSViewManager created the Health view");
133 [self.ckksViews addObject:self.healthView];
134 [self.ckksZones addObject:self.healthZoneID];
136 self.applepayZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"ApplePay" ownerName:CKCurrentUserDefaultName];
137 self.applepayZone = [[FakeCKZone alloc] initZone: self.applepayZoneID];
138 self.zones[self.applepayZoneID] = self.applepayZone;
139 self.applepayView = [[CKKSViewManager manager] findOrCreateView:@"ApplePay"];
140 XCTAssertNotNil(self.applepayView, "CKKSViewManager created the ApplePay view");
141 [self.ckksViews addObject:self.applepayView];
142 [self.ckksZones addObject:self.applepayZoneID];
144 self.homeZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Home" ownerName:CKCurrentUserDefaultName];
145 self.homeZone = [[FakeCKZone alloc] initZone: self.homeZoneID];
146 self.zones[self.homeZoneID] = self.homeZone;
147 self.homeView = [[CKKSViewManager manager] findOrCreateView:@"Home"];
148 XCTAssertNotNil(self.homeView, "CKKSViewManager created the Home view");
149 [self.ckksViews addObject:self.homeView];
150 [self.ckksZones addObject:self.homeZoneID];
152 self.limitedZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"LimitedPeersAllowed" ownerName:CKCurrentUserDefaultName];
153 self.limitedZone = [[FakeCKZone alloc] initZone: self.limitedZoneID];
154 self.zones[self.limitedZoneID] = self.limitedZone;
155 self.limitedView = [[CKKSViewManager manager] findOrCreateView:@"LimitedPeersAllowed"];
156 XCTAssertNotNil(self.limitedView, "should have a limited ckks view");
157 XCTAssertNotNil(self.limitedView, "CKKSViewManager created the LimitedPeersAllowed view");
158 [self.ckksViews addObject:self.limitedView];
159 [self.ckksZones addObject:self.limitedZoneID];
161 self.passwordsZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Passwords" ownerName:CKCurrentUserDefaultName];
162 self.passwordsZone = [[FakeCKZone alloc] initZone: self.passwordsZoneID];
163 self.zones[self.passwordsZoneID] = self.passwordsZone;
164 self.passwordsView = [[CKKSViewManager manager] findOrCreateView:@"Passwords"];
165 XCTAssertNotNil(self.passwordsView, "should have a passwords ckks view");
166 XCTAssertNotNil(self.passwordsView, "CKKSViewManager created the Passwords view");
167 [self.ckksViews addObject:self.passwordsView];
168 [self.ckksZones addObject:self.passwordsZoneID];
170 // These tests, at least, will use the policy codepaths!
171 [self.injectedManager setOverrideCKKSViewsFromPolicy:YES];
172 [self.injectedManager setCurrentSyncingPolicy:self.viewSortingPolicyForManagedViewList];
176 SecCKKSTestSetDisableSOS(true);
178 SecCKKSResetSyncing();
182 // If the test didn't already do this, allow each zone to spin up
183 self.accountStatus = CKAccountStatusNoAccount;
184 [self startCKKSSubsystem];
186 [self.engramView halt];
187 [self.engramView waitUntilAllOperationsAreFinished];
188 self.engramView = nil;
190 [self.manateeView halt];
191 [self.manateeView waitUntilAllOperationsAreFinished];
192 self.manateeView = nil;
194 [self.autoUnlockView halt];
195 [self.autoUnlockView waitUntilAllOperationsAreFinished];
196 self.autoUnlockView = nil;
198 [self.healthView halt];
199 [self.healthView waitUntilAllOperationsAreFinished];
200 self.healthView = nil;
202 [self.applepayView halt];
203 [self.applepayView waitUntilAllOperationsAreFinished];
204 self.applepayView = nil;
206 [self.homeView halt];
207 [self.homeView waitUntilAllOperationsAreFinished];
210 [self.limitedView halt];
211 [self.limitedView waitUntilAllOperationsAreFinished];
212 self.limitedView = nil;
214 [self.passwordsView halt];
215 [self.passwordsView waitUntilAllOperationsAreFinished];
216 self.passwordsView = nil;
221 - (ZoneKeys*)engramZoneKeys {
222 return self.keys[self.engramZoneID];
225 - (ZoneKeys*)manateeZoneKeys {
226 return self.keys[self.manateeZoneID];
229 - (void)saveFakeKeyHierarchiesToLocalDatabase {
230 for(CKRecordZoneID* zoneID in self.ckksZones) {
231 [self createAndSaveFakeKeyHierarchy: zoneID];
235 - (void)putFakeDeviceStatusesInCloudKit {
236 [self putFakeDeviceStatusInCloudKit: self.engramZoneID];
237 [self putFakeDeviceStatusInCloudKit: self.manateeZoneID];
238 [self putFakeDeviceStatusInCloudKit: self.autoUnlockZoneID];
239 [self putFakeDeviceStatusInCloudKit: self.healthZoneID];
240 [self putFakeDeviceStatusInCloudKit: self.applepayZoneID];
241 [self putFakeDeviceStatusInCloudKit: self.homeZoneID];
242 [self putFakeDeviceStatusInCloudKit: self.limitedZoneID];
243 [self putFakeDeviceStatusInCloudKit: self.passwordsZoneID];
246 - (void)putFakeKeyHierachiesInCloudKit{
247 [self putFakeKeyHierarchyInCloudKit: self.engramZoneID];
248 [self putFakeKeyHierarchyInCloudKit: self.manateeZoneID];
249 [self putFakeKeyHierarchyInCloudKit: self.autoUnlockZoneID];
250 [self putFakeKeyHierarchyInCloudKit: self.healthZoneID];
251 [self putFakeKeyHierarchyInCloudKit: self.applepayZoneID];
252 [self putFakeKeyHierarchyInCloudKit: self.homeZoneID];
253 [self putFakeKeyHierarchyInCloudKit: self.limitedZoneID];
254 [self putFakeKeyHierarchyInCloudKit: self.passwordsZoneID];
257 - (void)saveTLKsToKeychain{
258 [self saveTLKMaterialToKeychain:self.engramZoneID];
259 [self saveTLKMaterialToKeychain:self.manateeZoneID];
260 [self saveTLKMaterialToKeychain:self.autoUnlockZoneID];
261 [self saveTLKMaterialToKeychain:self.healthZoneID];
262 [self saveTLKMaterialToKeychain:self.applepayZoneID];
263 [self saveTLKMaterialToKeychain:self.homeZoneID];
264 [self saveTLKMaterialToKeychain:self.limitedZoneID];
265 [self saveTLKMaterialToKeychain:self.passwordsZoneID];
268 - (void)deleteTLKMaterialsFromKeychain{
269 [self deleteTLKMaterialFromKeychain: self.engramZoneID];
270 [self deleteTLKMaterialFromKeychain: self.manateeZoneID];
271 [self deleteTLKMaterialFromKeychain: self.autoUnlockZoneID];
272 [self deleteTLKMaterialFromKeychain: self.healthZoneID];
273 [self deleteTLKMaterialFromKeychain: self.applepayZoneID];
274 [self deleteTLKMaterialFromKeychain: self.homeZoneID];
275 [self deleteTLKMaterialFromKeychain:self.limitedZoneID];
276 [self deleteTLKMaterialFromKeychain:self.passwordsZoneID];
279 - (void)waitForKeyHierarchyReadinesses {
280 [self.manateeView waitForKeyHierarchyReadiness];
281 [self.engramView waitForKeyHierarchyReadiness];
282 [self.autoUnlockView waitForKeyHierarchyReadiness];
283 [self.healthView waitForKeyHierarchyReadiness];
284 [self.applepayView waitForKeyHierarchyReadiness];
285 [self.homeView waitForKeyHierarchyReadiness];
286 [self.limitedView waitForKeyHierarchyReadiness];
287 [self.passwordsView waitForKeyHierarchyReadiness];
290 - (void)expectCKKSTLKSelfShareUploads {
291 for(CKRecordZoneID* zoneID in self.ckksZones) {
292 [self expectCKKSTLKSelfShareUpload:zoneID];
299 @interface CloudKitKeychainSyncingMultiZoneTests : CloudKitKeychainSyncingMultiZoneTestsBase
302 @implementation CloudKitKeychainSyncingMultiZoneTests
304 - (void)testAllViewsMakeNewKeyHierarchies {
305 // Test starts with nothing anywhere
307 // Due to our new cross-zone fetch system, CKKS should only issue one fetch for all zones
308 // Since the tests can sometimes be slow, slow down the fetcher to normal speed
309 [self.injectedManager.zoneChangeFetcher.fetchScheduler changeDelays:2*NSEC_PER_SEC continuingDelay:30*NSEC_PER_SEC];
310 self.silentFetchesAllowed = false;
311 [self expectCKFetch];
313 [self startCKKSSubsystem];
314 [self performOctagonTLKUpload:self.ckksViews];
316 OCMVerifyAllWithDelay(self.mockDatabase, 20);
318 for(CKKSKeychainView* view in self.ckksViews) {
319 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should enter 'ready' for view %@", view);
323 - (void)testAllViewsAcceptExistingKeyHierarchies {
324 for(CKRecordZoneID* zoneID in self.ckksZones) {
325 [self putFakeKeyHierarchyInCloudKit:zoneID];
326 [self saveTLKMaterialToKeychain:zoneID];
327 [self expectCKKSTLKSelfShareUpload:zoneID];
330 [self.injectedManager.zoneChangeFetcher.fetchScheduler changeDelays:2*NSEC_PER_SEC continuingDelay:30*NSEC_PER_SEC];
331 self.silentFetchesAllowed = false;
332 [self expectCKFetch];
334 [self startCKKSSubsystem];
335 OCMVerifyAllWithDelay(self.mockDatabase, 20);
337 for(CKKSKeychainView* view in self.ckksViews) {
338 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should enter 'ready' for view %@", view);
342 - (void)testAddEngramManateeItems {
343 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
345 [self startCKKSSubsystem];
347 XCTestExpectation* engramChanged = [self expectChangeForView:self.engramZoneID.zoneName];
348 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
349 XCTestExpectation* manateeChanged = [self expectChangeForView:self.manateeZoneID.zoneName];
351 // We expect a single record to be uploaded to the engram view.
352 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.engramZoneID];
353 [self addGenericPassword: @"data" account: @"account-delete-me-engram" viewHint:(NSString*) kSecAttrViewHintEngram];
355 OCMVerifyAllWithDelay(self.mockDatabase, 20);
356 [self waitForExpectations:@[engramChanged] timeout:1];
357 [self waitForExpectations:@[pcsChanged] timeout:1];
359 pcsChanged = [self expectChangeForView:@"PCS"];
361 // We expect a single record to be uploaded to the manatee view.
362 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.manateeZoneID];
363 [self addGenericPassword: @"data" account: @"account-delete-me-manatee" viewHint:(NSString*) kSecAttrViewHintManatee];
365 OCMVerifyAllWithDelay(self.mockDatabase, 20);
366 [self waitForExpectations:@[manateeChanged] timeout:1];
367 [self waitForExpectations:@[pcsChanged] timeout:1];
370 - (void)testAddAutoUnlockItems {
371 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
373 [self startCKKSSubsystem];
375 XCTestExpectation* autoUnlockChanged = [self expectChangeForView:self.autoUnlockZoneID.zoneName];
376 // AutoUnlock is NOT is PCS view, so it should not send the fake 'PCS' view notification
377 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
378 pcsChanged.inverted = YES;
380 // We expect a single record to be uploaded to the AutoUnlock view.
381 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.autoUnlockZoneID];
382 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintAutoUnlock];
384 OCMVerifyAllWithDelay(self.mockDatabase, 20);
385 [self waitForExpectations:@[autoUnlockChanged] timeout:1];
386 [self waitForExpectations:@[pcsChanged] timeout:0.2];
389 - (void)testAddHealthItems {
390 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
392 [self startCKKSSubsystem];
394 XCTestExpectation* healthChanged = [self expectChangeForView:self.healthZoneID.zoneName];
395 // Health is NOT is PCS view, so it should not send the fake 'PCS' view notification
396 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
397 pcsChanged.inverted = YES;
399 // We expect a single record to be uploaded to the Health view.
400 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.healthZoneID];
401 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintHealth];
403 OCMVerifyAllWithDelay(self.mockDatabase, 20);
404 [self waitForExpectations:@[healthChanged] timeout:1];
405 [self waitForExpectations:@[pcsChanged] timeout:0.2];
408 - (void)testAddApplePayItems {
409 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
411 [self startCKKSSubsystem];
413 XCTestExpectation* applepayChanged = [self expectChangeForView:self.applepayZoneID.zoneName];
414 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
416 // We expect a single record to be uploaded to the ApplePay view.
417 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.applepayZoneID];
418 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintApplePay];
420 OCMVerifyAllWithDelay(self.mockDatabase, 20);
421 [self waitForExpectations:@[applepayChanged] timeout:1];
422 [self waitForExpectations:@[pcsChanged] timeout:0.2];
425 - (void)testAddHomeItems {
426 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
428 [self startCKKSSubsystem];
430 XCTestExpectation* homeChanged = [self expectChangeForView:self.homeZoneID.zoneName];
431 // Home is a now PCS view, so it should send the fake 'PCS' view notification
432 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
434 // We expect a single record to be uploaded to the ApplePay view.
435 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.homeZoneID];
436 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintHome];
438 OCMVerifyAllWithDelay(self.mockDatabase, 20);
439 [self waitForExpectations:@[homeChanged] timeout:1];
440 [self waitForExpectations:@[pcsChanged] timeout:0.2];
443 - (void)testAddLimitedPeersAllowedItems {
444 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
446 [self startCKKSSubsystem];
448 XCTestExpectation* limitedChanged = [self expectChangeForView:self.limitedZoneID.zoneName];
449 // LimitedPeersAllowed is a PCS view, so it should send the fake 'PCS' view notification
450 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
452 // We expect a single record to be uploaded to the LimitedPeersOkay view.
453 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.limitedZoneID];
454 [self addGenericPassword: @"data" account: @"account-delete-me-limited-peers" viewHint:(NSString*) kSecAttrViewHintLimitedPeersAllowed];
456 OCMVerifyAllWithDelay(self.mockDatabase, 20);
457 [self waitForExpectations:@[limitedChanged] timeout:1];
458 [self waitForExpectations:@[pcsChanged] timeout:0.2];
461 - (void)testMultipleZoneAdd {
462 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
464 // Let the horses loose
465 [self startCKKSSubsystem];
467 // We expect a single record to be uploaded to the 'LimitedPeersAllowed' view
468 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.limitedZoneID];
469 [self addGenericPassword: @"data" account: @"account-delete-me-limited-peers" viewHint:(NSString*)kSecAttrViewHintLimitedPeersAllowed];
470 OCMVerifyAllWithDelay(self.mockDatabase, 20);
472 // We expect a single record to be uploaded to the 'atv' home
473 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.homeZoneID];
474 [self addGenericPassword: @"data" account: @"account-delete-me-home" viewHint:(NSString*)kSecAttrViewHintHome];
476 OCMVerifyAllWithDelay(self.mockDatabase, 20);
477 OCMVerifyAllWithDelay(self.mockCKKSViewManager, 10);
480 - (void)testMultipleZoneDelete {
481 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
483 [self startCKKSSubsystem];
485 // We expect a single record to be uploaded.
486 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.limitedZoneID];
487 [self addGenericPassword: @"data" account: @"account-delete-me-limited-peers" viewHint:(NSString*)kSecAttrViewHintLimitedPeersAllowed];
488 OCMVerifyAllWithDelay(self.mockDatabase, 20);
490 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.homeZoneID];
491 [self addGenericPassword: @"data" account: @"account-delete-me-home" viewHint:(NSString*)kSecAttrViewHintHome];
492 OCMVerifyAllWithDelay(self.mockDatabase, 20);
493 [self waitForCKModifications];
495 // We expect a single record to be deleted from the ATV zone
496 [self expectCKDeleteItemRecords:1 zoneID:self.homeZoneID];
497 [self deleteGenericPassword:@"account-delete-me-home"];
498 OCMVerifyAllWithDelay(self.mockDatabase, 20);
500 // Now we expect a single record to be deleted from the test zone
501 [self expectCKDeleteItemRecords:1 zoneID:self.limitedZoneID];
502 [self deleteGenericPassword:@"account-delete-me-limited-peers"];
503 OCMVerifyAllWithDelay(self.mockDatabase, 20);
506 - (void)testAddAndReceiveDeleteForSafariPasswordItem {
507 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
509 [self startCKKSSubsystem];
511 XCTestExpectation* passwordChanged = [self expectChangeForView:self.passwordsView.zoneName];
513 // We expect a single record to be uploaded to the Passwords view.
514 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.passwordsZoneID];
516 [self addGenericPassword:@"data"
517 account:@"account-delete-me"
518 access:(id)kSecAttrAccessibleWhenUnlocked
520 accessGroup:@"com.apple.sbd"
521 expecting:errSecSuccess
522 message:@"Item for Password view"];
524 OCMVerifyAllWithDelay(self.mockDatabase, 20);
525 [self waitForExpectations:@[passwordChanged] timeout:1];
526 [self waitForCKModifications];
528 [self waitForKeyHierarchyReadinesses];
529 [self.passwordsView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
531 // Ensure that we catch up to the newest CK change token so our fake cloudkit will notice the delete at fetch time
532 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
533 [self.passwordsView waitForFetchAndIncomingQueueProcessing];
535 // Now, the item is deleted. Do we properly remove it?
536 CKRecord* itemRecord = nil;
537 for(CKRecord* record in [self.passwordsZone.currentDatabase allValues]) {
538 if([record.recordType isEqualToString:SecCKRecordItemType]) {
543 XCTAssertNotNil(itemRecord, "Should have found the item in the password zone");
545 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
546 (id)kSecAttrAccessGroup : @"com.apple.sbd",
547 (id)kSecAttrAccount : @"account-delete-me",
548 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
549 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
550 (id)kSecReturnData : (id)kCFBooleanTrue,
553 CFTypeRef item = NULL;
554 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
555 XCTAssertNotNil((__bridge id)item, "An item should have been found");
558 // Now, the item is deleted. The passwords view should delete the local item, even though it has the wrong 'vwht' on disk.
559 XCTAssertNotNil(self.passwordsZone.currentDatabase[itemRecord.recordID], "Record should exist in fake CK");
560 [self.passwordsZone deleteCKRecordIDFromZone:itemRecord.recordID];
562 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
563 [self.passwordsView waitForFetchAndIncomingQueueProcessing];
565 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
566 XCTAssertNil((__bridge id)item, "No item should have been found");
569 - (void)testAddOtherViewHintItem {
570 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
572 [self startCKKSSubsystem];
574 // We expect no uploads to CKKS.
575 [self addGenericPassword: @"data" account: @"account-delete-me-no-viewhint"];
576 [self addGenericPassword: @"data" account: @"account-delete-me-password" viewHint:(NSString*) kSOSViewBackupBagV0];
579 OCMVerifyAllWithDelay(self.mockDatabase, 20);
582 - (void)testUploadItemsAddedBeforeStart {
583 [self addGenericPassword:@"data"
584 account:@"account-delete-me"
585 access:(id)kSecAttrAccessibleAfterFirstUnlock
587 accessGroup:@"com.apple.sbd"
588 expecting:errSecSuccess
589 message:@"Item for Password view"];
591 [self addGenericPassword:@"data"
592 account:@"account-delete-me-2"
593 access:(id)kSecAttrAccessibleAfterFirstUnlock
595 accessGroup:@"com.apple.sbd"
596 expecting:errSecSuccess
597 message:@"Item for Password view"];
599 [self addGenericPassword:@"data" account:@"account-delete-me-limited-peers" viewHint:(NSString*)kSecAttrViewHintLimitedPeersAllowed];
601 NSError* error = nil;
602 NSDictionary* currentOQEs = [CKKSOutgoingQueueEntry countsByStateInZone:self.passwordsZoneID error:&error];
603 XCTAssertNil(error, "Should be no error counting OQEs");
604 XCTAssertEqual(0, currentOQEs.count, "Should be no OQEs");
606 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
608 // Now CKKS starts up
609 // Upon sign in, these items should be uploaded
610 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.passwordsZoneID
611 checkItem:[self checkClassCBlock:self.passwordsZoneID message:@"Object was encrypted under class C key in hierarchy"]];
612 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.limitedZoneID];
613 [self startCKKSSubsystem];
615 XCTAssertEqual(0, [self.passwordsView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
616 OCMVerifyAllWithDelay(self.mockDatabase, 20);
619 - (void)testReceiveItemInView {
620 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
621 [self startCKKSSubsystem];
623 [self waitForKeyHierarchyReadinesses];
624 [self.engramView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
626 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
628 CKRecord* ckr = [self createFakeRecord:self.engramZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
629 [self.engramZone addToZone: ckr];
631 XCTestExpectation* engramChanged = [self expectChangeForView:self.engramZoneID.zoneName];
632 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
634 self.silentFetchesAllowed = false;
635 [self expectCKFetch];
637 // Trigger a notification (with hilariously fake data)
638 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
640 OCMVerifyAllWithDelay(self.mockDatabase, 20);
641 [self.engramView waitForFetchAndIncomingQueueProcessing];
643 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
645 [self waitForExpectations:@[engramChanged] timeout:1];
646 [self waitForExpectations:@[pcsChanged] timeout:1];
649 - (void)testRecoverFromCloudKitOldChangeTokenInKeyHierarchyFetch {
650 [self putFakeKeyHierachiesInCloudKit];
651 [self saveTLKsToKeychain];
654 [self expectCKKSTLKSelfShareUploads];
656 // Spin up CKKS subsystem.
657 [self startCKKSSubsystem];
659 [self waitForKeyHierarchyReadinesses];
661 // We expect a single record to be uploaded
662 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
663 [self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
664 OCMVerifyAllWithDelay(self.mockDatabase, 20);
666 // Delete all old database states, to destroy the change tag validity
667 [self.manateeZone.pastDatabases removeAllObjects];
669 // We expect a total local flush and refetch
670 self.silentFetchesAllowed = false;
671 [self expectCKFetch]; // one to fail with a CKErrorChangeTokenExpired error
672 [self expectCKFetch]; // and one to succeed
674 [self.manateeView.stateMachine handleFlag:CKKSFlagFetchRequested];
676 XCTAssertEqual(0, [self.manateeView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
678 // Don't cause another fetch, because the machinery might not be ready
679 [self.manateeView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
681 OCMVerifyAllWithDelay(self.mockDatabase, 20);
683 // And check that a new upload happens just fine.
684 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.manateeZoneID checkItem: [self checkClassABlock:self.manateeZoneID message:@"Object was encrypted under class A key in hierarchy"]];
685 [self addGenericPassword:@"asdf"
686 account:@"account-class-A"
687 viewHint:(id)kSecAttrViewHintManatee
688 access:(id)kSecAttrAccessibleWhenUnlocked
689 expecting:errSecSuccess
690 message:@"Adding class A item"];
691 OCMVerifyAllWithDelay(self.mockDatabase, 20);
694 - (void)testResetAllCloudKitZones {
695 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
696 OCMExpect([self.suggestTLKUpload trigger]);
698 [self putFakeKeyHierachiesInCloudKit];
699 [self saveTLKsToKeychain];
700 [self expectCKKSTLKSelfShareUploads];
702 // Spin up CKKS subsystem.
703 [self startCKKSSubsystem];
704 [self waitForKeyHierarchyReadinesses];
706 // We expect a single record to be uploaded
707 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
708 [self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
709 OCMVerifyAllWithDelay(self.mockDatabase, 20);
710 [self waitForCKModifications];
712 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
714 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
716 // CKKS should issue exactly one deletion for all of these
717 self.silentZoneDeletesAllowed = true;
719 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
720 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-all-test" reply:^(NSError* result) {
721 XCTAssertNil(result, "no error resetting cloudkit");
722 ckksnotice_global("ckks", "Received a resetCloudKit callback");
723 [resetExpectation fulfill];
726 // Sneak in and perform Octagon's duties
727 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
728 [self performOctagonTLKUpload:self.ckksViews];
730 [self waitForExpectations:@[resetExpectation] timeout:20];
732 OCMVerifyAllWithDelay(self.mockDatabase, 20);
734 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem: [self checkClassABlock:self.manateeZoneID message:@"Object was encrypted under class A key in hierarchy"]];
735 [self addGenericPassword:@"asdf"
736 account:@"account-class-A"
737 viewHint:(id)kSecAttrViewHintManatee
738 access:(id)kSecAttrAccessibleWhenUnlocked
739 expecting:errSecSuccess
740 message:@"Adding class A item"];
741 OCMVerifyAllWithDelay(self.mockDatabase, 20);
744 - (void)testResetAllCloudKitZonesWithPartialZonesMissing {
745 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
746 OCMExpect([self.suggestTLKUpload trigger]);
748 [self putFakeKeyHierachiesInCloudKit];
749 [self saveTLKsToKeychain];
750 [self expectCKKSTLKSelfShareUploads];
752 // Spin up CKKS subsystem.
753 [self startCKKSSubsystem];
754 [self waitForKeyHierarchyReadinesses];
756 // We expect a single record to be uploaded
757 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
758 [self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
759 OCMVerifyAllWithDelay(self.mockDatabase, 20);
760 [self waitForCKModifications];
762 self.zones[self.manateeZoneID] = nil;
763 self.keys[self.manateeZoneID] = nil;
764 self.zones[self.applepayZoneID] = nil;
765 self.keys[self.applepayZoneID] = nil;
767 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
768 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
770 // CKKS should issue exactly one deletion for all of these
771 self.silentZoneDeletesAllowed = true;
773 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
774 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-all-test" reply:^(NSError* result) {
775 XCTAssertNil(result, "no error resetting cloudkit");
776 ckksnotice_global("ckks", "Received a resetCloudKit callback");
777 [resetExpectation fulfill];
780 // Sneak in and perform Octagon's duties
781 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
782 [self performOctagonTLKUpload:self.ckksViews];
784 [self waitForExpectations:@[resetExpectation] timeout:20];
786 OCMVerifyAllWithDelay(self.mockDatabase, 20);
788 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem: [self checkClassABlock:self.manateeZoneID message:@"Object was encrypted under class A key in hierarchy"]];
789 [self addGenericPassword:@"asdf"
790 account:@"account-class-A"
791 viewHint:(id)kSecAttrViewHintManatee
792 access:(id)kSecAttrAccessibleWhenUnlocked
793 expecting:errSecSuccess
794 message:@"Adding class A item"];
795 OCMVerifyAllWithDelay(self.mockDatabase, 20);
798 - (void)testResetMultiCloudKitZoneCloudKitRejects {
799 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
800 OCMExpect([self.suggestTLKUpload trigger]);
802 [self putFakeKeyHierachiesInCloudKit];
803 [self saveTLKsToKeychain];
804 [self expectCKKSTLKSelfShareUploads];
806 // Spin up CKKS subsystem.
807 [self startCKKSSubsystem];
808 [self waitForKeyHierarchyReadinesses];
810 // We expect a single record to be uploaded
811 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
812 [self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
813 OCMVerifyAllWithDelay(self.mockDatabase, 20);
814 [self waitForCKModifications];
816 self.nextModifyRecordZonesError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
819 CKErrorRetryAfterKey: @(0.2),
820 NSUnderlyingErrorKey: [[CKPrettyError alloc] initWithDomain:CKErrorDomain
824 self.silentZoneDeletesAllowed = true;
826 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
827 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
829 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
830 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
831 XCTAssertNil(result, "no error resetting cloudkit");
832 ckksnotice_global("ckks", "Received a resetCloudKit callback");
833 [resetExpectation fulfill];
836 // Sneak in and perform Octagon's duties
837 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
838 [self performOctagonTLKUpload:self.ckksViews];
840 [self waitForExpectations:@[resetExpectation] timeout:20];
842 OCMVerifyAllWithDelay(self.mockDatabase, 20);
844 XCTAssertNil(self.nextModifyRecordZonesError, "zone modification error should have been cleared");
847 - (void)testMultiZoneDeviceStateUploadGood {
848 [self putFakeKeyHierachiesInCloudKit];
849 [self saveTLKsToKeychain];
850 [self expectCKKSTLKSelfShareUploads];
852 // Spin up CKKS subsystem.
853 [self startCKKSSubsystem];
854 [self waitForKeyHierarchyReadinesses];
856 for(CKKSKeychainView* view in self.ckksViews) {
857 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
858 deletedRecordTypeCounts:nil
860 checkModifiedRecord:nil
861 runAfterModification:nil];
864 [self.injectedManager xpc24HrNotification];
866 OCMVerifyAllWithDelay(self.mockDatabase, 20);
869 - (void)testMultiZoneResync {
871 [self putFakeKeyHierachiesInCloudKit];
872 [self saveTLKsToKeychain];
873 [self expectCKKSTLKSelfShareUploads];
875 // Put sample data in zones, and save off a change token for later fetch shenanigans
876 [self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D00" withAccount:@"manatee0"]];
877 [self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D01" withAccount:@"manatee1"]];
878 CKServerChangeToken* manateeChangeToken1 = self.manateeZone.currentChangeToken;
879 [self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D02" withAccount:@"manatee2"]];
880 [self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D03" withAccount:@"manatee3"]];
882 [self.healthZone addToZone:[self createFakeRecord:self.healthZoneID recordName:@"7B598D31-0000-0000-FFFF-5A507ACB2D00" withAccount:@"health0"]];
883 [self.healthZone addToZone:[self createFakeRecord:self.healthZoneID recordName:@"7B598D31-0000-0000-FFFF-5A507ACB2D01" withAccount:@"health1"]];
885 [self startCKKSSubsystem];
886 [self waitForKeyHierarchyReadinesses];
888 OCMVerifyAllWithDelay(self.mockDatabase, 20);
890 [self.manateeView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
891 [self.healthView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
893 [self findGenericPassword:@"manatee0" expecting:errSecSuccess];
894 [self findGenericPassword:@"manatee1" expecting:errSecSuccess];
895 [self findGenericPassword:@"manatee2" expecting:errSecSuccess];
896 [self findGenericPassword:@"manatee3" expecting:errSecSuccess];
897 [self findGenericPassword:@"health0" expecting:errSecSuccess];
898 [self findGenericPassword:@"health1" expecting:errSecSuccess];
900 // Now, we resync. But, the manatee zone comes down in two fetches
901 self.silentFetchesAllowed = false;
902 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
903 // Assert that the fetch is a refetch
904 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.manateeZoneID].previousServerChangeToken;
905 if(changeToken == nil) {
910 } runBeforeFinished:^{}];
911 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
912 // Assert that the fetch is happening with the change token we paused at before
913 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.manateeZoneID].previousServerChangeToken;
914 if(changeToken && [changeToken isEqual:manateeChangeToken1]) {
919 } runBeforeFinished:^{}];
921 self.manateeZone.limitFetchTo = manateeChangeToken1;
923 // Attempt to trigger simultaneous key state resyncs. This is a horrible hack...
924 [self.manateeView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
925 self.manateeView.keyStateFullRefetchRequested = YES;
926 [self.manateeView _onqueuePokeKeyStateMachine];
927 return CKKSDatabaseTransactionCommit;
929 [self.healthView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
930 self.healthView.keyStateFullRefetchRequested = YES;
931 [self.healthView _onqueuePokeKeyStateMachine];
932 return CKKSDatabaseTransactionCommit;
935 OCMVerifyAllWithDelay(self.mockDatabase, 20);
937 [self.manateeView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
938 [self.healthView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
940 // And all items should still exist
941 [self findGenericPassword:@"manatee0" expecting:errSecSuccess];
942 [self findGenericPassword:@"manatee1" expecting:errSecSuccess];
943 [self findGenericPassword:@"manatee2" expecting:errSecSuccess];
944 [self findGenericPassword:@"manatee3" expecting:errSecSuccess];
945 [self findGenericPassword:@"health0" expecting:errSecSuccess];
946 [self findGenericPassword:@"health1" expecting:errSecSuccess];