4 #import <CloudKit/CloudKit.h>
5 #import <XCTest/XCTest.h>
6 #import <OCMock/OCMock.h>
9 #include <Security/SecItemPriv.h>
11 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
12 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
13 #import "keychain/ckks/CKKS.h"
14 #import "keychain/ckks/CKKSKeychainView.h"
15 #import "keychain/ckks/CKKSCurrentKeyPointer.h"
16 #import "keychain/ckks/CKKSItemEncrypter.h"
17 #import "keychain/ckks/CKKSKey.h"
18 #import "keychain/ckks/CKKSOutgoingQueueEntry.h"
19 #import "keychain/ckks/CKKSIncomingQueueEntry.h"
20 #import "keychain/ckks/CKKSSynchronizeOperation.h"
21 #import "keychain/ckks/CKKSViewManager.h"
22 #import "keychain/ckks/CKKSZoneStateEntry.h"
23 #import "keychain/ckks/CKKSManifest.h"
25 #import "keychain/ckks/tests/MockCloudKit.h"
27 #pragma clang diagnostic push
28 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
29 #include <Security/SecureObjectSync/SOSCloudCircle.h>
30 #include "keychain/SecureObjectSync/SOSAccountPriv.h"
31 #include "keychain/SecureObjectSync/SOSAccount.h"
32 #include "keychain/SecureObjectSync/SOSInternal.h"
33 #include "keychain/SecureObjectSync/SOSFullPeerInfo.h"
34 #pragma clang diagnostic pop
36 #include <Security/SecKey.h>
37 #include <Security/SecKeyPriv.h>
38 #pragma clang diagnostic pop
40 @interface CloudKitKeychainSyncingMultiZoneTestsBase : CloudKitKeychainSyncingMockXCTest
42 @property CKRecordZoneID* engramZoneID;
43 @property CKKSKeychainView* engramView;
44 @property FakeCKZone* engramZone;
45 @property (readonly) ZoneKeys* engramZoneKeys;
47 @property CKRecordZoneID* manateeZoneID;
48 @property CKKSKeychainView* manateeView;
49 @property FakeCKZone* manateeZone;
50 @property (readonly) ZoneKeys* manateeZoneKeys;
52 @property CKRecordZoneID* autoUnlockZoneID;
53 @property CKKSKeychainView* autoUnlockView;
54 @property FakeCKZone* autoUnlockZone;
55 @property (readonly) ZoneKeys* autoUnlockZoneKeys;
57 @property CKRecordZoneID* healthZoneID;
58 @property CKKSKeychainView* healthView;
59 @property FakeCKZone* healthZone;
60 @property (readonly) ZoneKeys* healthZoneKeys;
62 @property CKRecordZoneID* applepayZoneID;
63 @property CKKSKeychainView* applepayView;
64 @property FakeCKZone* applepayZone;
65 @property (readonly) ZoneKeys* applepayZoneKeys;
67 @property CKRecordZoneID* homeZoneID;
68 @property CKKSKeychainView* homeView;
69 @property FakeCKZone* homeZone;
70 @property (readonly) ZoneKeys* homeZoneKeys;
72 @property CKRecordZoneID* limitedZoneID;
73 @property CKKSKeychainView* limitedView;
74 @property FakeCKZone* limitedZone;
75 @property (readonly) ZoneKeys* limitedZoneKeys;
79 @implementation CloudKitKeychainSyncingMultiZoneTestsBase
82 SecCKKSResetSyncing();
87 SecCKKSSetSyncManifests(false);
88 SecCKKSSetEnforceManifests(false);
91 SecCKKSTestSetDisableSOS(false);
93 // Wait for the ViewManager to be brought up
94 XCTAssertEqual(0, [self.injectedManager.completedSecCKKSInitialize wait:20*NSEC_PER_SEC], "No timeout waiting for SecCKKSInitialize");
96 self.engramZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Engram" ownerName:CKCurrentUserDefaultName];
97 self.engramZone = [[FakeCKZone alloc] initZone: self.engramZoneID];
98 self.zones[self.engramZoneID] = self.engramZone;
99 self.engramView = [[CKKSViewManager manager] findOrCreateView:@"Engram"];
100 XCTAssertNotNil(self.engramView, "CKKSViewManager created the Engram view");
101 [self.ckksViews addObject:self.engramView];
102 [self.ckksZones addObject:self.engramZoneID];
104 self.manateeZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Manatee" ownerName:CKCurrentUserDefaultName];
105 self.manateeZone = [[FakeCKZone alloc] initZone: self.manateeZoneID];
106 self.zones[self.manateeZoneID] = self.manateeZone;
107 self.manateeView = [[CKKSViewManager manager] findOrCreateView:@"Manatee"];
108 XCTAssertNotNil(self.manateeView, "CKKSViewManager created the Manatee view");
109 [self.ckksViews addObject:self.manateeView];
110 [self.ckksZones addObject:self.manateeZoneID];
112 self.autoUnlockZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"AutoUnlock" ownerName:CKCurrentUserDefaultName];
113 self.autoUnlockZone = [[FakeCKZone alloc] initZone: self.autoUnlockZoneID];
114 self.zones[self.autoUnlockZoneID] = self.autoUnlockZone;
115 self.autoUnlockView = [[CKKSViewManager manager] findOrCreateView:@"AutoUnlock"];
116 XCTAssertNotNil(self.autoUnlockView, "CKKSViewManager created the AutoUnlock view");
117 [self.ckksViews addObject:self.autoUnlockView];
118 [self.ckksZones addObject:self.autoUnlockZoneID];
120 self.healthZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Health" ownerName:CKCurrentUserDefaultName];
121 self.healthZone = [[FakeCKZone alloc] initZone: self.healthZoneID];
122 self.zones[self.healthZoneID] = self.healthZone;
123 self.healthView = [[CKKSViewManager manager] findOrCreateView:@"Health"];
124 XCTAssertNotNil(self.healthView, "CKKSViewManager created the Health view");
125 [self.ckksViews addObject:self.healthView];
126 [self.ckksZones addObject:self.healthZoneID];
128 self.applepayZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"ApplePay" ownerName:CKCurrentUserDefaultName];
129 self.applepayZone = [[FakeCKZone alloc] initZone: self.applepayZoneID];
130 self.zones[self.applepayZoneID] = self.applepayZone;
131 self.applepayView = [[CKKSViewManager manager] findOrCreateView:@"ApplePay"];
132 XCTAssertNotNil(self.applepayView, "CKKSViewManager created the ApplePay view");
133 [self.ckksViews addObject:self.applepayView];
134 [self.ckksZones addObject:self.applepayZoneID];
136 self.homeZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Home" ownerName:CKCurrentUserDefaultName];
137 self.homeZone = [[FakeCKZone alloc] initZone: self.homeZoneID];
138 self.zones[self.homeZoneID] = self.homeZone;
139 self.homeView = [[CKKSViewManager manager] findOrCreateView:@"Home"];
140 XCTAssertNotNil(self.homeView, "CKKSViewManager created the Home view");
141 [self.ckksViews addObject:self.homeView];
142 [self.ckksZones addObject:self.homeZoneID];
144 self.limitedZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"LimitedPeersAllowed" ownerName:CKCurrentUserDefaultName];
145 self.limitedZone = [[FakeCKZone alloc] initZone: self.limitedZoneID];
146 self.zones[self.limitedZoneID] = self.limitedZone;
147 self.limitedView = [[CKKSViewManager manager] findOrCreateView:@"LimitedPeersAllowed"];
148 XCTAssertNotNil(self.limitedView, "should have a limited ckks view");
149 XCTAssertNotNil(self.limitedView, "CKKSViewManager created the LimitedPeersAllowed view");
150 [self.ckksViews addObject:self.limitedView];
151 [self.ckksZones addObject:self.limitedZoneID];
155 SecCKKSTestSetDisableSOS(true);
157 SecCKKSResetSyncing();
161 // If the test didn't already do this, allow each zone to spin up
162 self.accountStatus = CKAccountStatusNoAccount;
163 [self startCKKSSubsystem];
165 [self.engramView halt];
166 [self.engramView waitUntilAllOperationsAreFinished];
167 self.engramView = nil;
169 [self.manateeView halt];
170 [self.manateeView waitUntilAllOperationsAreFinished];
171 self.manateeView = nil;
173 [self.autoUnlockView halt];
174 [self.autoUnlockView waitUntilAllOperationsAreFinished];
175 self.autoUnlockView = nil;
177 [self.healthView halt];
178 [self.healthView waitUntilAllOperationsAreFinished];
179 self.healthView = nil;
181 [self.applepayView halt];
182 [self.applepayView waitUntilAllOperationsAreFinished];
183 self.applepayView = nil;
185 [self.homeView halt];
186 [self.homeView waitUntilAllOperationsAreFinished];
192 - (ZoneKeys*)engramZoneKeys {
193 return self.keys[self.engramZoneID];
196 - (ZoneKeys*)manateeZoneKeys {
197 return self.keys[self.manateeZoneID];
200 - (void)saveFakeKeyHierarchiesToLocalDatabase {
201 for(CKRecordZoneID* zoneID in self.ckksZones) {
202 [self createAndSaveFakeKeyHierarchy: zoneID];
206 - (void)putFakeDeviceStatusesInCloudKit {
207 [self putFakeDeviceStatusInCloudKit: self.engramZoneID];
208 [self putFakeDeviceStatusInCloudKit: self.manateeZoneID];
209 [self putFakeDeviceStatusInCloudKit: self.autoUnlockZoneID];
210 [self putFakeDeviceStatusInCloudKit: self.healthZoneID];
211 [self putFakeDeviceStatusInCloudKit: self.applepayZoneID];
212 [self putFakeDeviceStatusInCloudKit: self.homeZoneID];
213 [self putFakeDeviceStatusInCloudKit: self.limitedZoneID];
216 - (void)putFakeKeyHierachiesInCloudKit{
217 [self putFakeKeyHierarchyInCloudKit: self.engramZoneID];
218 [self putFakeKeyHierarchyInCloudKit: self.manateeZoneID];
219 [self putFakeKeyHierarchyInCloudKit: self.autoUnlockZoneID];
220 [self putFakeKeyHierarchyInCloudKit: self.healthZoneID];
221 [self putFakeKeyHierarchyInCloudKit: self.applepayZoneID];
222 [self putFakeKeyHierarchyInCloudKit: self.homeZoneID];
223 [self putFakeKeyHierarchyInCloudKit: self.limitedZoneID];
226 - (void)saveTLKsToKeychain{
227 [self saveTLKMaterialToKeychain:self.engramZoneID];
228 [self saveTLKMaterialToKeychain:self.manateeZoneID];
229 [self saveTLKMaterialToKeychain:self.autoUnlockZoneID];
230 [self saveTLKMaterialToKeychain:self.healthZoneID];
231 [self saveTLKMaterialToKeychain:self.applepayZoneID];
232 [self saveTLKMaterialToKeychain:self.homeZoneID];
233 [self saveTLKMaterialToKeychain:self.limitedZoneID];
236 - (void)deleteTLKMaterialsFromKeychain{
237 [self deleteTLKMaterialFromKeychain: self.engramZoneID];
238 [self deleteTLKMaterialFromKeychain: self.manateeZoneID];
239 [self deleteTLKMaterialFromKeychain: self.autoUnlockZoneID];
240 [self deleteTLKMaterialFromKeychain: self.healthZoneID];
241 [self deleteTLKMaterialFromKeychain: self.applepayZoneID];
242 [self deleteTLKMaterialFromKeychain: self.homeZoneID];
243 [self deleteTLKMaterialFromKeychain:self.limitedZoneID];
246 - (void)waitForKeyHierarchyReadinesses {
247 [self.manateeView waitForKeyHierarchyReadiness];
248 [self.engramView waitForKeyHierarchyReadiness];
249 [self.autoUnlockView waitForKeyHierarchyReadiness];
250 [self.healthView waitForKeyHierarchyReadiness];
251 [self.applepayView waitForKeyHierarchyReadiness];
252 [self.homeView waitForKeyHierarchyReadiness];
253 [self.limitedView waitForKeyHierarchyReadiness];
256 - (void)expectCKKSTLKSelfShareUploads {
257 for(CKRecordZoneID* zoneID in self.ckksZones) {
258 [self expectCKKSTLKSelfShareUpload:zoneID];
265 @interface CloudKitKeychainSyncingMultiZoneTests : CloudKitKeychainSyncingMultiZoneTestsBase
268 @implementation CloudKitKeychainSyncingMultiZoneTests
270 - (void)testAllViewsMakeNewKeyHierarchies {
271 // Test starts with nothing anywhere
273 // Due to our new cross-zone fetch system, CKKS should only issue one fetch for all zones
274 // Since the tests can sometimes be slow, slow down the fetcher to normal speed
275 [self.injectedManager.zoneChangeFetcher.fetchScheduler changeDelays:2*NSEC_PER_SEC continuingDelay:30*NSEC_PER_SEC];
276 self.silentFetchesAllowed = false;
277 [self expectCKFetch];
279 [self startCKKSSubsystem];
280 [self performOctagonTLKUpload:self.ckksViews];
282 OCMVerifyAllWithDelay(self.mockDatabase, 20);
284 for(CKKSKeychainView* view in self.ckksViews) {
285 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should enter 'ready' for view %@", view);
289 - (void)testAllViewsAcceptExistingKeyHierarchies {
290 for(CKRecordZoneID* zoneID in self.ckksZones) {
291 [self putFakeKeyHierarchyInCloudKit:zoneID];
292 [self saveTLKMaterialToKeychain:zoneID];
293 [self expectCKKSTLKSelfShareUpload:zoneID];
296 [self.injectedManager.zoneChangeFetcher.fetchScheduler changeDelays:2*NSEC_PER_SEC continuingDelay:30*NSEC_PER_SEC];
297 self.silentFetchesAllowed = false;
298 [self expectCKFetch];
300 [self startCKKSSubsystem];
301 OCMVerifyAllWithDelay(self.mockDatabase, 20);
303 for(CKKSKeychainView* view in self.ckksViews) {
304 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should enter 'ready' for view %@", view);
308 - (void)testAddEngramManateeItems {
309 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
311 [self startCKKSSubsystem];
313 XCTestExpectation* engramChanged = [self expectChangeForView:self.engramZoneID.zoneName];
314 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
315 XCTestExpectation* manateeChanged = [self expectChangeForView:self.manateeZoneID.zoneName];
317 // We expect a single record to be uploaded to the engram view.
318 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.engramZoneID];
319 [self addGenericPassword: @"data" account: @"account-delete-me-engram" viewHint:(NSString*) kSecAttrViewHintEngram];
321 OCMVerifyAllWithDelay(self.mockDatabase, 20);
322 [self waitForExpectations:@[engramChanged] timeout:1];
323 [self waitForExpectations:@[pcsChanged] timeout:1];
325 pcsChanged = [self expectChangeForView:@"PCS"];
327 // We expect a single record to be uploaded to the manatee view.
328 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.manateeZoneID];
329 [self addGenericPassword: @"data" account: @"account-delete-me-manatee" viewHint:(NSString*) kSecAttrViewHintManatee];
331 OCMVerifyAllWithDelay(self.mockDatabase, 20);
332 [self waitForExpectations:@[manateeChanged] timeout:1];
333 [self waitForExpectations:@[pcsChanged] timeout:1];
336 - (void)testAddAutoUnlockItems {
337 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
339 [self startCKKSSubsystem];
341 XCTestExpectation* autoUnlockChanged = [self expectChangeForView:self.autoUnlockZoneID.zoneName];
342 // AutoUnlock is NOT is PCS view, so it should not send the fake 'PCS' view notification
343 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
344 pcsChanged.inverted = YES;
346 // We expect a single record to be uploaded to the AutoUnlock view.
347 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.autoUnlockZoneID];
348 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintAutoUnlock];
350 OCMVerifyAllWithDelay(self.mockDatabase, 20);
351 [self waitForExpectations:@[autoUnlockChanged] timeout:1];
352 [self waitForExpectations:@[pcsChanged] timeout:0.2];
355 - (void)testAddHealthItems {
356 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
358 [self startCKKSSubsystem];
360 XCTestExpectation* healthChanged = [self expectChangeForView:self.healthZoneID.zoneName];
361 // Health is NOT is PCS view, so it should not send the fake 'PCS' view notification
362 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
363 pcsChanged.inverted = YES;
365 // We expect a single record to be uploaded to the Health view.
366 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.healthZoneID];
367 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintHealth];
369 OCMVerifyAllWithDelay(self.mockDatabase, 20);
370 [self waitForExpectations:@[healthChanged] timeout:1];
371 [self waitForExpectations:@[pcsChanged] timeout:0.2];
374 - (void)testAddApplePayItems {
375 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
377 [self startCKKSSubsystem];
379 XCTestExpectation* applepayChanged = [self expectChangeForView:self.applepayZoneID.zoneName];
380 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
382 // We expect a single record to be uploaded to the ApplePay view.
383 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.applepayZoneID];
384 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintApplePay];
386 OCMVerifyAllWithDelay(self.mockDatabase, 20);
387 [self waitForExpectations:@[applepayChanged] timeout:1];
388 [self waitForExpectations:@[pcsChanged] timeout:0.2];
391 - (void)testAddHomeItems {
392 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
394 [self startCKKSSubsystem];
396 XCTestExpectation* homeChanged = [self expectChangeForView:self.homeZoneID.zoneName];
397 // Home is a now PCS view, so it should send the fake 'PCS' view notification
398 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
400 // We expect a single record to be uploaded to the ApplePay view.
401 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.homeZoneID];
402 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintHome];
404 OCMVerifyAllWithDelay(self.mockDatabase, 20);
405 [self waitForExpectations:@[homeChanged] timeout:1];
406 [self waitForExpectations:@[pcsChanged] timeout:0.2];
409 - (void)testAddLimitedPeersAllowedItems {
410 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
412 [self startCKKSSubsystem];
414 XCTestExpectation* limitedChanged = [self expectChangeForView:self.limitedZoneID.zoneName];
415 // LimitedPeersAllowed is a PCS view, so it should send the fake 'PCS' view notification
416 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
418 // We expect a single record to be uploaded to the LimitedPeersOkay view.
419 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.limitedZoneID];
420 [self addGenericPassword: @"data" account: @"account-delete-me-limited-peers" viewHint:(NSString*) kSecAttrViewHintLimitedPeersAllowed];
422 OCMVerifyAllWithDelay(self.mockDatabase, 20);
423 [self waitForExpectations:@[limitedChanged] timeout:1];
424 [self waitForExpectations:@[pcsChanged] timeout:0.2];
427 - (void)testAddOtherViewHintItem {
428 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
430 [self startCKKSSubsystem];
432 // We expect no uploads to CKKS.
433 [self addGenericPassword: @"data" account: @"account-delete-me-no-viewhint"];
434 [self addGenericPassword: @"data" account: @"account-delete-me-password" viewHint:(NSString*) kSOSViewAutofillPasswords];
437 OCMVerifyAllWithDelay(self.mockDatabase, 20);
440 - (void)testReceiveItemInView {
441 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
442 [self startCKKSSubsystem];
444 for(CKRecordZoneID* zoneID in self.ckksZones) {
445 [self expectCKKSTLKSelfShareUpload:zoneID];
448 [self waitForKeyHierarchyReadinesses];
450 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
452 CKRecord* ckr = [self createFakeRecord:self.engramZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
453 [self.engramZone addToZone: ckr];
455 XCTestExpectation* engramChanged = [self expectChangeForView:self.engramZoneID.zoneName];
456 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
458 // Trigger a notification (with hilariously fake data)
459 [self.engramView notifyZoneChange:nil];
461 [self.engramView waitForFetchAndIncomingQueueProcessing];
462 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
464 [self waitForExpectations:@[engramChanged] timeout:1];
465 [self waitForExpectations:@[pcsChanged] timeout:1];
468 - (void)testRecoverFromCloudKitOldChangeTokenInKeyHierarchyFetch {
469 [self putFakeKeyHierachiesInCloudKit];
470 [self saveTLKsToKeychain];
473 [self expectCKKSTLKSelfShareUploads];
475 // Spin up CKKS subsystem.
476 [self startCKKSSubsystem];
478 [self waitForKeyHierarchyReadinesses];
480 // We expect a single record to be uploaded
481 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
482 [self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
483 OCMVerifyAllWithDelay(self.mockDatabase, 20);
485 // Delete all old database states, to destroy the change tag validity
486 [self.manateeZone.pastDatabases removeAllObjects];
488 // We expect a total local flush and refetch
489 self.silentFetchesAllowed = false;
490 [self expectCKFetch]; // one to fail with a CKErrorChangeTokenExpired error
491 [self expectCKFetch]; // and one to succeed
493 [self.manateeView dispatchSyncWithAccountKeys: ^bool {
494 [self.manateeView _onqueueKeyStateMachineRequestFetch];
498 XCTAssertEqual(0, [self.manateeView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
500 [self.manateeView waitForFetchAndIncomingQueueProcessing];
502 OCMVerifyAllWithDelay(self.mockDatabase, 20);
504 // And check that a new upload happens just fine.
505 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.manateeZoneID checkItem: [self checkClassABlock:self.manateeZoneID message:@"Object was encrypted under class A key in hierarchy"]];
506 [self addGenericPassword:@"asdf"
507 account:@"account-class-A"
508 viewHint:(id)kSecAttrViewHintManatee
509 access:(id)kSecAttrAccessibleWhenUnlocked
510 expecting:errSecSuccess
511 message:@"Adding class A item"];
512 OCMVerifyAllWithDelay(self.mockDatabase, 20);
515 - (void)testResetAllCloudKitZones {
516 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
517 OCMExpect([self.suggestTLKUpload trigger]);
519 [self putFakeKeyHierachiesInCloudKit];
520 [self saveTLKsToKeychain];
521 [self expectCKKSTLKSelfShareUploads];
523 // Spin up CKKS subsystem.
524 [self startCKKSSubsystem];
525 [self waitForKeyHierarchyReadinesses];
527 // We expect a single record to be uploaded
528 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
529 [self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
530 OCMVerifyAllWithDelay(self.mockDatabase, 20);
531 [self waitForCKModifications];
533 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
535 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
537 // CKKS should issue exactly one deletion for all of these
538 self.silentZoneDeletesAllowed = true;
540 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
541 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-all-test" reply:^(NSError* result) {
542 XCTAssertNil(result, "no error resetting cloudkit");
543 secnotice("ckks", "Received a resetCloudKit callback");
544 [resetExpectation fulfill];
547 // Sneak in and perform Octagon's duties
548 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
549 [self performOctagonTLKUpload:self.ckksViews];
551 [self waitForExpectations:@[resetExpectation] timeout:20];
553 OCMVerifyAllWithDelay(self.mockDatabase, 20);
555 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem: [self checkClassABlock:self.manateeZoneID message:@"Object was encrypted under class A key in hierarchy"]];
556 [self addGenericPassword:@"asdf"
557 account:@"account-class-A"
558 viewHint:(id)kSecAttrViewHintManatee
559 access:(id)kSecAttrAccessibleWhenUnlocked
560 expecting:errSecSuccess
561 message:@"Adding class A item"];
562 OCMVerifyAllWithDelay(self.mockDatabase, 20);
565 - (void)testResetAllCloudKitZonesWithPartialZonesMissing {
566 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
567 OCMExpect([self.suggestTLKUpload trigger]);
569 [self putFakeKeyHierachiesInCloudKit];
570 [self saveTLKsToKeychain];
571 [self expectCKKSTLKSelfShareUploads];
573 // Spin up CKKS subsystem.
574 [self startCKKSSubsystem];
575 [self waitForKeyHierarchyReadinesses];
577 // We expect a single record to be uploaded
578 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
579 [self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
580 OCMVerifyAllWithDelay(self.mockDatabase, 20);
581 [self waitForCKModifications];
583 self.zones[self.manateeZoneID] = nil;
584 self.keys[self.manateeZoneID] = nil;
585 self.zones[self.applepayZoneID] = nil;
586 self.keys[self.applepayZoneID] = nil;
588 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
589 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
591 // CKKS should issue exactly one deletion for all of these
592 self.silentZoneDeletesAllowed = true;
594 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
595 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-all-test" reply:^(NSError* result) {
596 XCTAssertNil(result, "no error resetting cloudkit");
597 secnotice("ckks", "Received a resetCloudKit callback");
598 [resetExpectation fulfill];
601 // Sneak in and perform Octagon's duties
602 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
603 [self performOctagonTLKUpload:self.ckksViews];
605 [self waitForExpectations:@[resetExpectation] timeout:20];
607 OCMVerifyAllWithDelay(self.mockDatabase, 20);
609 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem: [self checkClassABlock:self.manateeZoneID message:@"Object was encrypted under class A key in hierarchy"]];
610 [self addGenericPassword:@"asdf"
611 account:@"account-class-A"
612 viewHint:(id)kSecAttrViewHintManatee
613 access:(id)kSecAttrAccessibleWhenUnlocked
614 expecting:errSecSuccess
615 message:@"Adding class A item"];
616 OCMVerifyAllWithDelay(self.mockDatabase, 20);
619 - (void)testResetMultiCloudKitZoneCloudKitRejects {
620 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
621 OCMExpect([self.suggestTLKUpload trigger]);
623 [self putFakeKeyHierachiesInCloudKit];
624 [self saveTLKsToKeychain];
625 [self expectCKKSTLKSelfShareUploads];
627 // Spin up CKKS subsystem.
628 [self startCKKSSubsystem];
629 [self waitForKeyHierarchyReadinesses];
631 // We expect a single record to be uploaded
632 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
633 [self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
634 OCMVerifyAllWithDelay(self.mockDatabase, 20);
635 [self waitForCKModifications];
637 self.nextModifyRecordZonesError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
640 CKErrorRetryAfterKey: @(0.2),
641 NSUnderlyingErrorKey: [[CKPrettyError alloc] initWithDomain:CKErrorDomain
645 self.silentZoneDeletesAllowed = true;
647 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
648 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
650 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
651 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
652 XCTAssertNil(result, "no error resetting cloudkit");
653 secnotice("ckks", "Received a resetCloudKit callback");
654 [resetExpectation fulfill];
657 // Sneak in and perform Octagon's duties
658 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
659 [self performOctagonTLKUpload:self.ckksViews];
661 [self waitForExpectations:@[resetExpectation] timeout:20];
663 OCMVerifyAllWithDelay(self.mockDatabase, 20);
665 XCTAssertNil(self.nextModifyRecordZonesError, "zone modification error should have been cleared");
668 - (void)testMultiZoneDeviceStateUploadGood {
669 [self putFakeKeyHierachiesInCloudKit];
670 [self saveTLKsToKeychain];
671 [self expectCKKSTLKSelfShareUploads];
673 // Spin up CKKS subsystem.
674 [self startCKKSSubsystem];
675 [self waitForKeyHierarchyReadinesses];
677 for(CKKSKeychainView* view in self.ckksViews) {
678 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
679 deletedRecordTypeCounts:nil
681 checkModifiedRecord:nil
682 runAfterModification:nil];
685 [self.injectedManager xpc24HrNotification];
687 OCMVerifyAllWithDelay(self.mockDatabase, 20);