]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTests+MultiZone.m
Security-59306.101.1.tar.gz
[apple/security.git] / keychain / ckks / tests / CKKSTests+MultiZone.m
1
2 #if OCTAGON
3
4 #import <CloudKit/CloudKit.h>
5 #import <XCTest/XCTest.h>
6 #import <OCMock/OCMock.h>
7 #import <notify.h>
8
9 #include <Security/SecItemPriv.h>
10 #import <TrustedPeers/TrustedPeers.h>
11 #import <TrustedPeers/TPPBPolicyKeyViewMapping.h>
12 #import <TrustedPeers/TPDictionaryMatchingRules.h>
13
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/CKKSSynchronizeOperation.h"
24 #import "keychain/ckks/CKKSViewManager.h"
25 #import "keychain/ckks/CKKSZoneStateEntry.h"
26 #import "keychain/ckks/CKKSManifest.h"
27
28 #import "keychain/ckks/tests/CKKSTests+MultiZone.h"
29 #import "keychain/ckks/tests/MockCloudKit.h"
30
31 #pragma clang diagnostic push
32 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
33 #include <Security/SecureObjectSync/SOSCloudCircle.h>
34 #include "keychain/SecureObjectSync/SOSAccountPriv.h"
35 #include "keychain/SecureObjectSync/SOSAccount.h"
36 #include "keychain/SecureObjectSync/SOSInternal.h"
37 #include "keychain/SecureObjectSync/SOSFullPeerInfo.h"
38 #pragma clang diagnostic pop
39
40 #include <Security/SecKey.h>
41 #include <Security/SecKeyPriv.h>
42 #pragma clang diagnostic pop
43
44 @interface CloudKitKeychainSyncingMultiZoneTestsBase ()
45 @end
46
47 @implementation CloudKitKeychainSyncingMultiZoneTestsBase
48 + (void)setUp {
49 SecCKKSEnable();
50 SecCKKSResetSyncing();
51 [super setUp];
52 }
53
54 - (NSSet*)managedViewList {
55 NSMutableSet* parentSet = [[super managedViewList] mutableCopy];
56 [parentSet addObject:@"SafariPasswords"];
57 return parentSet;
58 }
59
60 // Make a policy as normal for most views, but Passwords is special
61 - (TPPolicy*)viewSortingPolicyForManagedViewList
62 {
63 NSMutableArray<TPPBPolicyKeyViewMapping*>* rules = [NSMutableArray array];
64
65 for(NSString* viewName in self.managedViewList) {
66 TPPBPolicyKeyViewMapping* mapping = [[TPPBPolicyKeyViewMapping alloc] init];
67 mapping.view = viewName;
68
69 // 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)
70 if([viewName isEqualToString:@"SafariPasswords"]) {
71 mapping.matchingRule = [TPDictionaryMatchingRule fieldMatch:@"agrp"
72 fieldRegex:[NSString stringWithFormat:@"^com\\.apple\\.sbd$"]];
73 } else {
74 mapping.matchingRule = [TPDictionaryMatchingRule fieldMatch:@"vwht"
75 fieldRegex:[NSString stringWithFormat:@"^%@$", viewName]];
76 }
77
78 [rules addObject:mapping];
79 }
80
81 TPPolicy* policy = [TPPolicy policyWithModelToCategory:@[]
82 categoriesByView:@{}
83 introducersByCategory:@{}
84 keyViewMapping:rules
85 unknownRedactions:NO
86 version:[[TPPolicyVersion alloc] initWithVersion:1 hash:@"fake-policy-for-views"]];
87
88 return policy;
89 }
90
91 - (void)setUp {
92 SecCKKSSetSyncManifests(false);
93 SecCKKSSetEnforceManifests(false);
94
95 [super setUp];
96 SecCKKSTestSetDisableSOS(false);
97
98 // Wait for the ViewManager to be brought up
99 XCTAssertEqual(0, [self.injectedManager.completedSecCKKSInitialize wait:20*NSEC_PER_SEC], "No timeout waiting for SecCKKSInitialize");
100
101 self.engramZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Engram" ownerName:CKCurrentUserDefaultName];
102 self.engramZone = [[FakeCKZone alloc] initZone: self.engramZoneID];
103 self.zones[self.engramZoneID] = self.engramZone;
104 self.engramView = [[CKKSViewManager manager] findOrCreateView:@"Engram"];
105 XCTAssertNotNil(self.engramView, "CKKSViewManager created the Engram view");
106 [self.ckksViews addObject:self.engramView];
107 [self.ckksZones addObject:self.engramZoneID];
108
109 self.manateeZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Manatee" ownerName:CKCurrentUserDefaultName];
110 self.manateeZone = [[FakeCKZone alloc] initZone: self.manateeZoneID];
111 self.zones[self.manateeZoneID] = self.manateeZone;
112 self.manateeView = [[CKKSViewManager manager] findOrCreateView:@"Manatee"];
113 XCTAssertNotNil(self.manateeView, "CKKSViewManager created the Manatee view");
114 [self.ckksViews addObject:self.manateeView];
115 [self.ckksZones addObject:self.manateeZoneID];
116
117 self.autoUnlockZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"AutoUnlock" ownerName:CKCurrentUserDefaultName];
118 self.autoUnlockZone = [[FakeCKZone alloc] initZone: self.autoUnlockZoneID];
119 self.zones[self.autoUnlockZoneID] = self.autoUnlockZone;
120 self.autoUnlockView = [[CKKSViewManager manager] findOrCreateView:@"AutoUnlock"];
121 XCTAssertNotNil(self.autoUnlockView, "CKKSViewManager created the AutoUnlock view");
122 [self.ckksViews addObject:self.autoUnlockView];
123 [self.ckksZones addObject:self.autoUnlockZoneID];
124
125 self.healthZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Health" ownerName:CKCurrentUserDefaultName];
126 self.healthZone = [[FakeCKZone alloc] initZone: self.healthZoneID];
127 self.zones[self.healthZoneID] = self.healthZone;
128 self.healthView = [[CKKSViewManager manager] findOrCreateView:@"Health"];
129 XCTAssertNotNil(self.healthView, "CKKSViewManager created the Health view");
130 [self.ckksViews addObject:self.healthView];
131 [self.ckksZones addObject:self.healthZoneID];
132
133 self.applepayZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"ApplePay" ownerName:CKCurrentUserDefaultName];
134 self.applepayZone = [[FakeCKZone alloc] initZone: self.applepayZoneID];
135 self.zones[self.applepayZoneID] = self.applepayZone;
136 self.applepayView = [[CKKSViewManager manager] findOrCreateView:@"ApplePay"];
137 XCTAssertNotNil(self.applepayView, "CKKSViewManager created the ApplePay view");
138 [self.ckksViews addObject:self.applepayView];
139 [self.ckksZones addObject:self.applepayZoneID];
140
141 self.homeZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Home" ownerName:CKCurrentUserDefaultName];
142 self.homeZone = [[FakeCKZone alloc] initZone: self.homeZoneID];
143 self.zones[self.homeZoneID] = self.homeZone;
144 self.homeView = [[CKKSViewManager manager] findOrCreateView:@"Home"];
145 XCTAssertNotNil(self.homeView, "CKKSViewManager created the Home view");
146 [self.ckksViews addObject:self.homeView];
147 [self.ckksZones addObject:self.homeZoneID];
148
149 self.limitedZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"LimitedPeersAllowed" ownerName:CKCurrentUserDefaultName];
150 self.limitedZone = [[FakeCKZone alloc] initZone: self.limitedZoneID];
151 self.zones[self.limitedZoneID] = self.limitedZone;
152 self.limitedView = [[CKKSViewManager manager] findOrCreateView:@"LimitedPeersAllowed"];
153 XCTAssertNotNil(self.limitedView, "should have a limited ckks view");
154 XCTAssertNotNil(self.limitedView, "CKKSViewManager created the LimitedPeersAllowed view");
155 [self.ckksViews addObject:self.limitedView];
156 [self.ckksZones addObject:self.limitedZoneID];
157
158 self.passwordsZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"SafariPasswords" ownerName:CKCurrentUserDefaultName];
159 self.passwordsZone = [[FakeCKZone alloc] initZone: self.passwordsZoneID];
160 self.zones[self.passwordsZoneID] = self.passwordsZone;
161 self.passwordsView = [[CKKSViewManager manager] findOrCreateView:@"SafariPasswords"];
162 XCTAssertNotNil(self.passwordsView, "should have a passwords ckks view");
163 XCTAssertNotNil(self.passwordsView, "CKKSViewManager created the Passwords view");
164 [self.ckksViews addObject:self.passwordsView];
165 [self.ckksZones addObject:self.passwordsZoneID];
166
167 // These tests, at least, will use the policy codepaths!
168 [self.injectedManager setOverrideCKKSViewsFromPolicy:YES];
169 [self.injectedManager setSyncingViews:self.managedViewList sortingPolicy:self.viewSortingPolicyForManagedViewList];
170 }
171
172 + (void)tearDown {
173 SecCKKSTestSetDisableSOS(true);
174 [super tearDown];
175 SecCKKSResetSyncing();
176 }
177
178 - (void)tearDown {
179 // If the test didn't already do this, allow each zone to spin up
180 self.accountStatus = CKAccountStatusNoAccount;
181 [self startCKKSSubsystem];
182
183 [self.engramView halt];
184 [self.engramView waitUntilAllOperationsAreFinished];
185 self.engramView = nil;
186
187 [self.manateeView halt];
188 [self.manateeView waitUntilAllOperationsAreFinished];
189 self.manateeView = nil;
190
191 [self.autoUnlockView halt];
192 [self.autoUnlockView waitUntilAllOperationsAreFinished];
193 self.autoUnlockView = nil;
194
195 [self.healthView halt];
196 [self.healthView waitUntilAllOperationsAreFinished];
197 self.healthView = nil;
198
199 [self.applepayView halt];
200 [self.applepayView waitUntilAllOperationsAreFinished];
201 self.applepayView = nil;
202
203 [self.homeView halt];
204 [self.homeView waitUntilAllOperationsAreFinished];
205 self.homeView = nil;
206
207 [self.limitedView halt];
208 [self.limitedView waitUntilAllOperationsAreFinished];
209 self.limitedView = nil;
210
211 [self.passwordsView halt];
212 [self.passwordsView waitUntilAllOperationsAreFinished];
213 self.passwordsView = nil;
214
215 [super tearDown];
216 }
217
218 - (ZoneKeys*)engramZoneKeys {
219 return self.keys[self.engramZoneID];
220 }
221
222 - (ZoneKeys*)manateeZoneKeys {
223 return self.keys[self.manateeZoneID];
224 }
225
226 - (void)saveFakeKeyHierarchiesToLocalDatabase {
227 for(CKRecordZoneID* zoneID in self.ckksZones) {
228 [self createAndSaveFakeKeyHierarchy: zoneID];
229 }
230 }
231
232 - (void)putFakeDeviceStatusesInCloudKit {
233 [self putFakeDeviceStatusInCloudKit: self.engramZoneID];
234 [self putFakeDeviceStatusInCloudKit: self.manateeZoneID];
235 [self putFakeDeviceStatusInCloudKit: self.autoUnlockZoneID];
236 [self putFakeDeviceStatusInCloudKit: self.healthZoneID];
237 [self putFakeDeviceStatusInCloudKit: self.applepayZoneID];
238 [self putFakeDeviceStatusInCloudKit: self.homeZoneID];
239 [self putFakeDeviceStatusInCloudKit: self.limitedZoneID];
240 [self putFakeDeviceStatusInCloudKit: self.passwordsZoneID];
241 }
242
243 - (void)putFakeKeyHierachiesInCloudKit{
244 [self putFakeKeyHierarchyInCloudKit: self.engramZoneID];
245 [self putFakeKeyHierarchyInCloudKit: self.manateeZoneID];
246 [self putFakeKeyHierarchyInCloudKit: self.autoUnlockZoneID];
247 [self putFakeKeyHierarchyInCloudKit: self.healthZoneID];
248 [self putFakeKeyHierarchyInCloudKit: self.applepayZoneID];
249 [self putFakeKeyHierarchyInCloudKit: self.homeZoneID];
250 [self putFakeKeyHierarchyInCloudKit: self.limitedZoneID];
251 [self putFakeKeyHierarchyInCloudKit: self.passwordsZoneID];
252 }
253
254 - (void)saveTLKsToKeychain{
255 [self saveTLKMaterialToKeychain:self.engramZoneID];
256 [self saveTLKMaterialToKeychain:self.manateeZoneID];
257 [self saveTLKMaterialToKeychain:self.autoUnlockZoneID];
258 [self saveTLKMaterialToKeychain:self.healthZoneID];
259 [self saveTLKMaterialToKeychain:self.applepayZoneID];
260 [self saveTLKMaterialToKeychain:self.homeZoneID];
261 [self saveTLKMaterialToKeychain:self.limitedZoneID];
262 [self saveTLKMaterialToKeychain:self.passwordsZoneID];
263 }
264
265 - (void)deleteTLKMaterialsFromKeychain{
266 [self deleteTLKMaterialFromKeychain: self.engramZoneID];
267 [self deleteTLKMaterialFromKeychain: self.manateeZoneID];
268 [self deleteTLKMaterialFromKeychain: self.autoUnlockZoneID];
269 [self deleteTLKMaterialFromKeychain: self.healthZoneID];
270 [self deleteTLKMaterialFromKeychain: self.applepayZoneID];
271 [self deleteTLKMaterialFromKeychain: self.homeZoneID];
272 [self deleteTLKMaterialFromKeychain:self.limitedZoneID];
273 [self deleteTLKMaterialFromKeychain:self.passwordsZoneID];
274 }
275
276 - (void)waitForKeyHierarchyReadinesses {
277 [self.manateeView waitForKeyHierarchyReadiness];
278 [self.engramView waitForKeyHierarchyReadiness];
279 [self.autoUnlockView waitForKeyHierarchyReadiness];
280 [self.healthView waitForKeyHierarchyReadiness];
281 [self.applepayView waitForKeyHierarchyReadiness];
282 [self.homeView waitForKeyHierarchyReadiness];
283 [self.limitedView waitForKeyHierarchyReadiness];
284 [self.passwordsView waitForKeyHierarchyReadiness];
285 }
286
287 - (void)expectCKKSTLKSelfShareUploads {
288 for(CKRecordZoneID* zoneID in self.ckksZones) {
289 [self expectCKKSTLKSelfShareUpload:zoneID];
290 }
291
292 }
293
294 @end
295
296 @interface CloudKitKeychainSyncingMultiZoneTests : CloudKitKeychainSyncingMultiZoneTestsBase
297 @end
298
299 @implementation CloudKitKeychainSyncingMultiZoneTests
300
301 - (void)testAllViewsMakeNewKeyHierarchies {
302 // Test starts with nothing anywhere
303
304 // Due to our new cross-zone fetch system, CKKS should only issue one fetch for all zones
305 // Since the tests can sometimes be slow, slow down the fetcher to normal speed
306 [self.injectedManager.zoneChangeFetcher.fetchScheduler changeDelays:2*NSEC_PER_SEC continuingDelay:30*NSEC_PER_SEC];
307 self.silentFetchesAllowed = false;
308 [self expectCKFetch];
309
310 [self startCKKSSubsystem];
311 [self performOctagonTLKUpload:self.ckksViews];
312
313 OCMVerifyAllWithDelay(self.mockDatabase, 20);
314
315 for(CKKSKeychainView* view in self.ckksViews) {
316 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should enter 'ready' for view %@", view);
317 }
318 }
319
320 - (void)testAllViewsAcceptExistingKeyHierarchies {
321 for(CKRecordZoneID* zoneID in self.ckksZones) {
322 [self putFakeKeyHierarchyInCloudKit:zoneID];
323 [self saveTLKMaterialToKeychain:zoneID];
324 [self expectCKKSTLKSelfShareUpload:zoneID];
325 }
326
327 [self.injectedManager.zoneChangeFetcher.fetchScheduler changeDelays:2*NSEC_PER_SEC continuingDelay:30*NSEC_PER_SEC];
328 self.silentFetchesAllowed = false;
329 [self expectCKFetch];
330
331 [self startCKKSSubsystem];
332 OCMVerifyAllWithDelay(self.mockDatabase, 20);
333
334 for(CKKSKeychainView* view in self.ckksViews) {
335 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should enter 'ready' for view %@", view);
336 }
337 }
338
339 - (void)testAddEngramManateeItems {
340 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
341
342 [self startCKKSSubsystem];
343
344 XCTestExpectation* engramChanged = [self expectChangeForView:self.engramZoneID.zoneName];
345 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
346 XCTestExpectation* manateeChanged = [self expectChangeForView:self.manateeZoneID.zoneName];
347
348 // We expect a single record to be uploaded to the engram view.
349 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.engramZoneID];
350 [self addGenericPassword: @"data" account: @"account-delete-me-engram" viewHint:(NSString*) kSecAttrViewHintEngram];
351
352 OCMVerifyAllWithDelay(self.mockDatabase, 20);
353 [self waitForExpectations:@[engramChanged] timeout:1];
354 [self waitForExpectations:@[pcsChanged] timeout:1];
355
356 pcsChanged = [self expectChangeForView:@"PCS"];
357
358 // We expect a single record to be uploaded to the manatee view.
359 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.manateeZoneID];
360 [self addGenericPassword: @"data" account: @"account-delete-me-manatee" viewHint:(NSString*) kSecAttrViewHintManatee];
361
362 OCMVerifyAllWithDelay(self.mockDatabase, 20);
363 [self waitForExpectations:@[manateeChanged] timeout:1];
364 [self waitForExpectations:@[pcsChanged] timeout:1];
365 }
366
367 - (void)testAddAutoUnlockItems {
368 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
369
370 [self startCKKSSubsystem];
371
372 XCTestExpectation* autoUnlockChanged = [self expectChangeForView:self.autoUnlockZoneID.zoneName];
373 // AutoUnlock is NOT is PCS view, so it should not send the fake 'PCS' view notification
374 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
375 pcsChanged.inverted = YES;
376
377 // We expect a single record to be uploaded to the AutoUnlock view.
378 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.autoUnlockZoneID];
379 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintAutoUnlock];
380
381 OCMVerifyAllWithDelay(self.mockDatabase, 20);
382 [self waitForExpectations:@[autoUnlockChanged] timeout:1];
383 [self waitForExpectations:@[pcsChanged] timeout:0.2];
384 }
385
386 - (void)testAddHealthItems {
387 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
388
389 [self startCKKSSubsystem];
390
391 XCTestExpectation* healthChanged = [self expectChangeForView:self.healthZoneID.zoneName];
392 // Health is NOT is PCS view, so it should not send the fake 'PCS' view notification
393 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
394 pcsChanged.inverted = YES;
395
396 // We expect a single record to be uploaded to the Health view.
397 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.healthZoneID];
398 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintHealth];
399
400 OCMVerifyAllWithDelay(self.mockDatabase, 20);
401 [self waitForExpectations:@[healthChanged] timeout:1];
402 [self waitForExpectations:@[pcsChanged] timeout:0.2];
403 }
404
405 - (void)testAddApplePayItems {
406 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
407
408 [self startCKKSSubsystem];
409
410 XCTestExpectation* applepayChanged = [self expectChangeForView:self.applepayZoneID.zoneName];
411 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
412
413 // We expect a single record to be uploaded to the ApplePay view.
414 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.applepayZoneID];
415 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintApplePay];
416
417 OCMVerifyAllWithDelay(self.mockDatabase, 20);
418 [self waitForExpectations:@[applepayChanged] timeout:1];
419 [self waitForExpectations:@[pcsChanged] timeout:0.2];
420 }
421
422 - (void)testAddHomeItems {
423 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
424
425 [self startCKKSSubsystem];
426
427 XCTestExpectation* homeChanged = [self expectChangeForView:self.homeZoneID.zoneName];
428 // Home is a now PCS view, so it should send the fake 'PCS' view notification
429 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
430
431 // We expect a single record to be uploaded to the ApplePay view.
432 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.homeZoneID];
433 [self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintHome];
434
435 OCMVerifyAllWithDelay(self.mockDatabase, 20);
436 [self waitForExpectations:@[homeChanged] timeout:1];
437 [self waitForExpectations:@[pcsChanged] timeout:0.2];
438 }
439
440 - (void)testAddLimitedPeersAllowedItems {
441 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
442
443 [self startCKKSSubsystem];
444
445 XCTestExpectation* limitedChanged = [self expectChangeForView:self.limitedZoneID.zoneName];
446 // LimitedPeersAllowed is a PCS view, so it should send the fake 'PCS' view notification
447 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
448
449 // We expect a single record to be uploaded to the LimitedPeersOkay view.
450 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.limitedZoneID];
451 [self addGenericPassword: @"data" account: @"account-delete-me-limited-peers" viewHint:(NSString*) kSecAttrViewHintLimitedPeersAllowed];
452
453 OCMVerifyAllWithDelay(self.mockDatabase, 20);
454 [self waitForExpectations:@[limitedChanged] timeout:1];
455 [self waitForExpectations:@[pcsChanged] timeout:0.2];
456 }
457
458 - (void)testMultipleZoneAdd {
459 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
460
461 // Let the horses loose
462 [self startCKKSSubsystem];
463
464 // We expect a single record to be uploaded to the 'LimitedPeersAllowed' view
465 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.limitedZoneID];
466 [self addGenericPassword: @"data" account: @"account-delete-me-limited-peers" viewHint:(NSString*)kSecAttrViewHintLimitedPeersAllowed];
467 OCMVerifyAllWithDelay(self.mockDatabase, 20);
468
469 // We expect a single record to be uploaded to the 'atv' home
470 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.homeZoneID];
471 [self addGenericPassword: @"data" account: @"account-delete-me-home" viewHint:(NSString*)kSecAttrViewHintHome];
472
473 OCMVerifyAllWithDelay(self.mockDatabase, 20);
474 OCMVerifyAllWithDelay(self.mockCKKSViewManager, 10);
475 }
476
477 - (void)testMultipleZoneDelete {
478 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
479
480 [self startCKKSSubsystem];
481
482 // We expect a single record to be uploaded.
483 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.limitedZoneID];
484 [self addGenericPassword: @"data" account: @"account-delete-me-limited-peers" viewHint:(NSString*)kSecAttrViewHintLimitedPeersAllowed];
485 OCMVerifyAllWithDelay(self.mockDatabase, 20);
486
487 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.homeZoneID];
488 [self addGenericPassword: @"data" account: @"account-delete-me-home" viewHint:(NSString*)kSecAttrViewHintHome];
489 OCMVerifyAllWithDelay(self.mockDatabase, 20);
490 [self waitForCKModifications];
491
492 // We expect a single record to be deleted from the ATV zone
493 [self expectCKDeleteItemRecords:1 zoneID:self.homeZoneID];
494 [self deleteGenericPassword:@"account-delete-me-home"];
495 OCMVerifyAllWithDelay(self.mockDatabase, 20);
496
497 // Now we expect a single record to be deleted from the test zone
498 [self expectCKDeleteItemRecords:1 zoneID:self.limitedZoneID];
499 [self deleteGenericPassword:@"account-delete-me-limited-peers"];
500 OCMVerifyAllWithDelay(self.mockDatabase, 20);
501 }
502
503 - (void)testAddAndReceiveDeleteForSafariPasswordItem {
504 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
505
506 [self startCKKSSubsystem];
507
508 XCTestExpectation* passwordChanged = [self expectChangeForView:self.passwordsView.zoneName];
509
510 // We expect a single record to be uploaded to the Passwords view.
511 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.passwordsZoneID];
512
513 [self addGenericPassword:@"data"
514 account:@"account-delete-me"
515 access:(id)kSecAttrAccessibleWhenUnlocked
516 viewHint:nil
517 accessGroup:@"com.apple.sbd"
518 expecting:errSecSuccess
519 message:@"Item for Password view"];
520
521 OCMVerifyAllWithDelay(self.mockDatabase, 20);
522 [self waitForExpectations:@[passwordChanged] timeout:1];
523 [self waitForCKModifications];
524
525 [self waitForKeyHierarchyReadinesses];
526 [self.passwordsView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
527
528 // Now, the item is deleted. Do we properly remove it?
529 CKRecord* itemRecord = nil;
530 for(CKRecord* record in [self.passwordsZone.currentDatabase allValues]) {
531 if([record.recordType isEqualToString:SecCKRecordItemType]) {
532 itemRecord = record;
533 break;
534 }
535 }
536 XCTAssertNotNil(itemRecord, "Should have found the item in the password zone");
537
538 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
539 (id)kSecAttrAccessGroup : @"com.apple.sbd",
540 (id)kSecAttrAccount : @"account-delete-me",
541 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
542 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
543 (id)kSecReturnData : (id)kCFBooleanTrue,
544 };
545
546 CFTypeRef item = NULL;
547 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
548 XCTAssertNotNil((__bridge id)item, "No item should have been found");
549 CFReleaseNull(item);
550
551 // Now, the item is deleted. The passwords view should delete the local item, even though it has the wrong 'vwht' on disk.
552 [self.passwordsZone deleteCKRecordIDFromZone:itemRecord.recordID];
553 [self.passwordsView notifyZoneChange:nil];
554 [self.passwordsView waitForFetchAndIncomingQueueProcessing];
555
556 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
557 XCTAssertNil((__bridge id)item, "No item should have been found");
558 }
559
560 - (void)testAddOtherViewHintItem {
561 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
562
563 [self startCKKSSubsystem];
564
565 // We expect no uploads to CKKS.
566 [self addGenericPassword: @"data" account: @"account-delete-me-no-viewhint"];
567 [self addGenericPassword: @"data" account: @"account-delete-me-password" viewHint:(NSString*) kSOSViewBackupBagV0];
568
569 sleep(1);
570 OCMVerifyAllWithDelay(self.mockDatabase, 20);
571 }
572
573 - (void)testUploadItemsAddedBeforeStart {
574 [self addGenericPassword:@"data"
575 account:@"account-delete-me"
576 access:(id)kSecAttrAccessibleAfterFirstUnlock
577 viewHint:nil
578 accessGroup:@"com.apple.sbd"
579 expecting:errSecSuccess
580 message:@"Item for Password view"];
581
582 [self addGenericPassword:@"data"
583 account:@"account-delete-me-2"
584 access:(id)kSecAttrAccessibleAfterFirstUnlock
585 viewHint:nil
586 accessGroup:@"com.apple.sbd"
587 expecting:errSecSuccess
588 message:@"Item for Password view"];
589
590 [self addGenericPassword:@"data" account:@"account-delete-me-limited-peers" viewHint:(NSString*)kSecAttrViewHintLimitedPeersAllowed];
591
592 NSError* error = nil;
593 NSDictionary* currentOQEs = [CKKSOutgoingQueueEntry countsByStateInZone:self.passwordsZoneID error:&error];
594 XCTAssertNil(error, "Should be no error counting OQEs");
595 XCTAssertEqual(0, currentOQEs.count, "Should be no OQEs");
596
597 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
598
599 // Now CKKS starts up
600 // Upon sign in, these items should be uploaded
601 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.passwordsZoneID
602 checkItem:[self checkClassCBlock:self.passwordsZoneID message:@"Object was encrypted under class C key in hierarchy"]];
603 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.limitedZoneID];
604 [self startCKKSSubsystem];
605
606 XCTAssertEqual(0, [self.passwordsView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
607 OCMVerifyAllWithDelay(self.mockDatabase, 20);
608 }
609
610 - (void)testReceiveItemInView {
611 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
612 [self startCKKSSubsystem];
613
614 for(CKRecordZoneID* zoneID in self.ckksZones) {
615 [self expectCKKSTLKSelfShareUpload:zoneID];
616 }
617
618 [self waitForKeyHierarchyReadinesses];
619
620 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
621
622 CKRecord* ckr = [self createFakeRecord:self.engramZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
623 [self.engramZone addToZone: ckr];
624
625 XCTestExpectation* engramChanged = [self expectChangeForView:self.engramZoneID.zoneName];
626 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
627
628 // Trigger a notification (with hilariously fake data)
629 [self.engramView notifyZoneChange:nil];
630
631 [self.engramView waitForFetchAndIncomingQueueProcessing];
632 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
633
634 [self waitForExpectations:@[engramChanged] timeout:1];
635 [self waitForExpectations:@[pcsChanged] timeout:1];
636 }
637
638 - (void)testRecoverFromCloudKitOldChangeTokenInKeyHierarchyFetch {
639 [self putFakeKeyHierachiesInCloudKit];
640 [self saveTLKsToKeychain];
641
642
643 [self expectCKKSTLKSelfShareUploads];
644
645 // Spin up CKKS subsystem.
646 [self startCKKSSubsystem];
647
648 [self waitForKeyHierarchyReadinesses];
649
650 // We expect a single record to be uploaded
651 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
652 [self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
653 OCMVerifyAllWithDelay(self.mockDatabase, 20);
654
655 // Delete all old database states, to destroy the change tag validity
656 [self.manateeZone.pastDatabases removeAllObjects];
657
658 // We expect a total local flush and refetch
659 self.silentFetchesAllowed = false;
660 [self expectCKFetch]; // one to fail with a CKErrorChangeTokenExpired error
661 [self expectCKFetch]; // and one to succeed
662
663 [self.manateeView dispatchSyncWithAccountKeys: ^bool {
664 [self.manateeView _onqueueKeyStateMachineRequestFetch];
665 return true;
666 }];
667
668 XCTAssertEqual(0, [self.manateeView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
669
670 [self.manateeView waitForFetchAndIncomingQueueProcessing];
671
672 OCMVerifyAllWithDelay(self.mockDatabase, 20);
673
674 // And check that a new upload happens just fine.
675 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.manateeZoneID checkItem: [self checkClassABlock:self.manateeZoneID message:@"Object was encrypted under class A key in hierarchy"]];
676 [self addGenericPassword:@"asdf"
677 account:@"account-class-A"
678 viewHint:(id)kSecAttrViewHintManatee
679 access:(id)kSecAttrAccessibleWhenUnlocked
680 expecting:errSecSuccess
681 message:@"Adding class A item"];
682 OCMVerifyAllWithDelay(self.mockDatabase, 20);
683 }
684
685 - (void)testResetAllCloudKitZones {
686 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
687 OCMExpect([self.suggestTLKUpload trigger]);
688
689 [self putFakeKeyHierachiesInCloudKit];
690 [self saveTLKsToKeychain];
691 [self expectCKKSTLKSelfShareUploads];
692
693 // Spin up CKKS subsystem.
694 [self startCKKSSubsystem];
695 [self waitForKeyHierarchyReadinesses];
696
697 // We expect a single record to be uploaded
698 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
699 [self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
700 OCMVerifyAllWithDelay(self.mockDatabase, 20);
701 [self waitForCKModifications];
702
703 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
704
705 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
706
707 // CKKS should issue exactly one deletion for all of these
708 self.silentZoneDeletesAllowed = true;
709
710 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
711 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-all-test" reply:^(NSError* result) {
712 XCTAssertNil(result, "no error resetting cloudkit");
713 secnotice("ckks", "Received a resetCloudKit callback");
714 [resetExpectation fulfill];
715 }];
716
717 // Sneak in and perform Octagon's duties
718 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
719 [self performOctagonTLKUpload:self.ckksViews];
720
721 [self waitForExpectations:@[resetExpectation] timeout:20];
722
723 OCMVerifyAllWithDelay(self.mockDatabase, 20);
724
725 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem: [self checkClassABlock:self.manateeZoneID message:@"Object was encrypted under class A key in hierarchy"]];
726 [self addGenericPassword:@"asdf"
727 account:@"account-class-A"
728 viewHint:(id)kSecAttrViewHintManatee
729 access:(id)kSecAttrAccessibleWhenUnlocked
730 expecting:errSecSuccess
731 message:@"Adding class A item"];
732 OCMVerifyAllWithDelay(self.mockDatabase, 20);
733 }
734
735 - (void)testResetAllCloudKitZonesWithPartialZonesMissing {
736 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
737 OCMExpect([self.suggestTLKUpload trigger]);
738
739 [self putFakeKeyHierachiesInCloudKit];
740 [self saveTLKsToKeychain];
741 [self expectCKKSTLKSelfShareUploads];
742
743 // Spin up CKKS subsystem.
744 [self startCKKSSubsystem];
745 [self waitForKeyHierarchyReadinesses];
746
747 // We expect a single record to be uploaded
748 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
749 [self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
750 OCMVerifyAllWithDelay(self.mockDatabase, 20);
751 [self waitForCKModifications];
752
753 self.zones[self.manateeZoneID] = nil;
754 self.keys[self.manateeZoneID] = nil;
755 self.zones[self.applepayZoneID] = nil;
756 self.keys[self.applepayZoneID] = nil;
757
758 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
759 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
760
761 // CKKS should issue exactly one deletion for all of these
762 self.silentZoneDeletesAllowed = true;
763
764 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
765 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-all-test" reply:^(NSError* result) {
766 XCTAssertNil(result, "no error resetting cloudkit");
767 secnotice("ckks", "Received a resetCloudKit callback");
768 [resetExpectation fulfill];
769 }];
770
771 // Sneak in and perform Octagon's duties
772 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
773 [self performOctagonTLKUpload:self.ckksViews];
774
775 [self waitForExpectations:@[resetExpectation] timeout:20];
776
777 OCMVerifyAllWithDelay(self.mockDatabase, 20);
778
779 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem: [self checkClassABlock:self.manateeZoneID message:@"Object was encrypted under class A key in hierarchy"]];
780 [self addGenericPassword:@"asdf"
781 account:@"account-class-A"
782 viewHint:(id)kSecAttrViewHintManatee
783 access:(id)kSecAttrAccessibleWhenUnlocked
784 expecting:errSecSuccess
785 message:@"Adding class A item"];
786 OCMVerifyAllWithDelay(self.mockDatabase, 20);
787 }
788
789 - (void)testResetMultiCloudKitZoneCloudKitRejects {
790 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
791 OCMExpect([self.suggestTLKUpload trigger]);
792
793 [self putFakeKeyHierachiesInCloudKit];
794 [self saveTLKsToKeychain];
795 [self expectCKKSTLKSelfShareUploads];
796
797 // Spin up CKKS subsystem.
798 [self startCKKSSubsystem];
799 [self waitForKeyHierarchyReadinesses];
800
801 // We expect a single record to be uploaded
802 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
803 [self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
804 OCMVerifyAllWithDelay(self.mockDatabase, 20);
805 [self waitForCKModifications];
806
807 self.nextModifyRecordZonesError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
808 code:CKErrorZoneBusy
809 userInfo:@{
810 CKErrorRetryAfterKey: @(0.2),
811 NSUnderlyingErrorKey: [[CKPrettyError alloc] initWithDomain:CKErrorDomain
812 code:2029
813 userInfo:nil],
814 }];
815 self.silentZoneDeletesAllowed = true;
816
817 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
818 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
819
820 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
821 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
822 XCTAssertNil(result, "no error resetting cloudkit");
823 secnotice("ckks", "Received a resetCloudKit callback");
824 [resetExpectation fulfill];
825 }];
826
827 // Sneak in and perform Octagon's duties
828 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
829 [self performOctagonTLKUpload:self.ckksViews];
830
831 [self waitForExpectations:@[resetExpectation] timeout:20];
832
833 OCMVerifyAllWithDelay(self.mockDatabase, 20);
834
835 XCTAssertNil(self.nextModifyRecordZonesError, "zone modification error should have been cleared");
836 }
837
838 - (void)testMultiZoneDeviceStateUploadGood {
839 [self putFakeKeyHierachiesInCloudKit];
840 [self saveTLKsToKeychain];
841 [self expectCKKSTLKSelfShareUploads];
842
843 // Spin up CKKS subsystem.
844 [self startCKKSSubsystem];
845 [self waitForKeyHierarchyReadinesses];
846
847 for(CKKSKeychainView* view in self.ckksViews) {
848 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
849 deletedRecordTypeCounts:nil
850 zoneID:view.zoneID
851 checkModifiedRecord:nil
852 runAfterModification:nil];
853 }
854
855 [self.injectedManager xpc24HrNotification];
856
857 OCMVerifyAllWithDelay(self.mockDatabase, 20);
858 }
859
860 - (void)testMultiZoneResync {
861 // Set up
862 [self putFakeKeyHierachiesInCloudKit];
863 [self saveTLKsToKeychain];
864 [self expectCKKSTLKSelfShareUploads];
865
866 // Put sample data in zones, and save off a change token for later fetch shenanigans
867 [self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D00" withAccount:@"manatee0"]];
868 [self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D01" withAccount:@"manatee1"]];
869 CKServerChangeToken* manateeChangeToken1 = self.manateeZone.currentChangeToken;
870 [self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D02" withAccount:@"manatee2"]];
871 [self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D03" withAccount:@"manatee3"]];
872
873 [self.healthZone addToZone:[self createFakeRecord:self.healthZoneID recordName:@"7B598D31-0000-0000-FFFF-5A507ACB2D00" withAccount:@"health0"]];
874 [self.healthZone addToZone:[self createFakeRecord:self.healthZoneID recordName:@"7B598D31-0000-0000-FFFF-5A507ACB2D01" withAccount:@"health1"]];
875
876 [self startCKKSSubsystem];
877 [self waitForKeyHierarchyReadinesses];
878
879 OCMVerifyAllWithDelay(self.mockDatabase, 20);
880
881 [self.manateeView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
882 [self.healthView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
883
884 [self findGenericPassword:@"manatee0" expecting:errSecSuccess];
885 [self findGenericPassword:@"manatee1" expecting:errSecSuccess];
886 [self findGenericPassword:@"manatee2" expecting:errSecSuccess];
887 [self findGenericPassword:@"manatee3" expecting:errSecSuccess];
888 [self findGenericPassword:@"health0" expecting:errSecSuccess];
889 [self findGenericPassword:@"health1" expecting:errSecSuccess];
890
891 // Now, we resync. But, the manatee zone comes down in two fetches
892 self.silentFetchesAllowed = false;
893 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
894 // Assert that the fetch is a refetch
895 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.manateeZoneID].previousServerChangeToken;
896 if(changeToken == nil) {
897 return YES;
898 } else {
899 return NO;
900 }
901 } runBeforeFinished:^{}];
902 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
903 // Assert that the fetch is happening with the change token we paused at before
904 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.manateeZoneID].previousServerChangeToken;
905 if(changeToken && [changeToken isEqual:manateeChangeToken1]) {
906 return YES;
907 } else {
908 return NO;
909 }
910 } runBeforeFinished:^{}];
911
912 self.manateeZone.limitFetchTo = manateeChangeToken1;
913
914 // Attempt to trigger simultaneous key state resyncs. This is a horrible hack...
915 [self.manateeView dispatchSyncWithAccountKeys:^bool {
916 self.manateeView.keyStateFullRefetchRequested = YES;
917 [self.manateeView _onqueueAdvanceKeyStateMachineToState:nil withError:nil];
918 return true;
919 }];
920 [self.healthView dispatchSyncWithAccountKeys:^bool {
921 self.healthView.keyStateFullRefetchRequested = YES;
922 [self.healthView _onqueueAdvanceKeyStateMachineToState:nil withError:nil];
923 return true;
924 }];
925
926 OCMVerifyAllWithDelay(self.mockDatabase, 20);
927
928 [self.manateeView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
929 [self.healthView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
930
931 // And all items should still exist
932 [self findGenericPassword:@"manatee0" expecting:errSecSuccess];
933 [self findGenericPassword:@"manatee1" expecting:errSecSuccess];
934 [self findGenericPassword:@"manatee2" expecting:errSecSuccess];
935 [self findGenericPassword:@"manatee3" expecting:errSecSuccess];
936 [self findGenericPassword:@"health0" expecting:errSecSuccess];
937 [self findGenericPassword:@"health1" expecting:errSecSuccess];
938 }
939
940 @end
941
942 #endif // OCTAGON
943