]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTests+MultiZone.m
Security-59306.41.2.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
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"
24
25 #import "keychain/ckks/tests/MockCloudKit.h"
26
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
35
36 #include <Security/SecKey.h>
37 #include <Security/SecKeyPriv.h>
38 #pragma clang diagnostic pop
39
40 @interface CloudKitKeychainSyncingMultiZoneTestsBase : CloudKitKeychainSyncingMockXCTest
41
42 @property CKRecordZoneID* engramZoneID;
43 @property CKKSKeychainView* engramView;
44 @property FakeCKZone* engramZone;
45 @property (readonly) ZoneKeys* engramZoneKeys;
46
47 @property CKRecordZoneID* manateeZoneID;
48 @property CKKSKeychainView* manateeView;
49 @property FakeCKZone* manateeZone;
50 @property (readonly) ZoneKeys* manateeZoneKeys;
51
52 @property CKRecordZoneID* autoUnlockZoneID;
53 @property CKKSKeychainView* autoUnlockView;
54 @property FakeCKZone* autoUnlockZone;
55 @property (readonly) ZoneKeys* autoUnlockZoneKeys;
56
57 @property CKRecordZoneID* healthZoneID;
58 @property CKKSKeychainView* healthView;
59 @property FakeCKZone* healthZone;
60 @property (readonly) ZoneKeys* healthZoneKeys;
61
62 @property CKRecordZoneID* applepayZoneID;
63 @property CKKSKeychainView* applepayView;
64 @property FakeCKZone* applepayZone;
65 @property (readonly) ZoneKeys* applepayZoneKeys;
66
67 @property CKRecordZoneID* homeZoneID;
68 @property CKKSKeychainView* homeView;
69 @property FakeCKZone* homeZone;
70 @property (readonly) ZoneKeys* homeZoneKeys;
71
72 @property CKRecordZoneID* limitedZoneID;
73 @property CKKSKeychainView* limitedView;
74 @property FakeCKZone* limitedZone;
75 @property (readonly) ZoneKeys* limitedZoneKeys;
76
77 @end
78
79 @implementation CloudKitKeychainSyncingMultiZoneTestsBase
80 + (void)setUp {
81 SecCKKSEnable();
82 SecCKKSResetSyncing();
83 [super setUp];
84 }
85
86 - (void)setUp {
87 SecCKKSSetSyncManifests(false);
88 SecCKKSSetEnforceManifests(false);
89
90 [super setUp];
91 SecCKKSTestSetDisableSOS(false);
92
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");
95
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];
103
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];
111
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];
119
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];
127
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];
135
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];
143
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];
152 }
153
154 + (void)tearDown {
155 SecCKKSTestSetDisableSOS(true);
156 [super tearDown];
157 SecCKKSResetSyncing();
158 }
159
160 - (void)tearDown {
161 // If the test didn't already do this, allow each zone to spin up
162 self.accountStatus = CKAccountStatusNoAccount;
163 [self startCKKSSubsystem];
164
165 [self.engramView halt];
166 [self.engramView waitUntilAllOperationsAreFinished];
167 self.engramView = nil;
168
169 [self.manateeView halt];
170 [self.manateeView waitUntilAllOperationsAreFinished];
171 self.manateeView = nil;
172
173 [self.autoUnlockView halt];
174 [self.autoUnlockView waitUntilAllOperationsAreFinished];
175 self.autoUnlockView = nil;
176
177 [self.healthView halt];
178 [self.healthView waitUntilAllOperationsAreFinished];
179 self.healthView = nil;
180
181 [self.applepayView halt];
182 [self.applepayView waitUntilAllOperationsAreFinished];
183 self.applepayView = nil;
184
185 [self.homeView halt];
186 [self.homeView waitUntilAllOperationsAreFinished];
187 self.homeView = nil;
188
189 [super tearDown];
190 }
191
192 - (ZoneKeys*)engramZoneKeys {
193 return self.keys[self.engramZoneID];
194 }
195
196 - (ZoneKeys*)manateeZoneKeys {
197 return self.keys[self.manateeZoneID];
198 }
199
200 - (void)saveFakeKeyHierarchiesToLocalDatabase {
201 for(CKRecordZoneID* zoneID in self.ckksZones) {
202 [self createAndSaveFakeKeyHierarchy: zoneID];
203 }
204 }
205
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];
214 }
215
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];
224 }
225
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];
234 }
235
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];
244 }
245
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];
254 }
255
256 - (void)expectCKKSTLKSelfShareUploads {
257 for(CKRecordZoneID* zoneID in self.ckksZones) {
258 [self expectCKKSTLKSelfShareUpload:zoneID];
259 }
260
261 }
262
263 @end
264
265 @interface CloudKitKeychainSyncingMultiZoneTests : CloudKitKeychainSyncingMultiZoneTestsBase
266 @end
267
268 @implementation CloudKitKeychainSyncingMultiZoneTests
269
270 - (void)testAllViewsMakeNewKeyHierarchies {
271 // Test starts with nothing anywhere
272
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];
278
279 [self startCKKSSubsystem];
280 [self performOctagonTLKUpload:self.ckksViews];
281
282 OCMVerifyAllWithDelay(self.mockDatabase, 20);
283
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);
286 }
287 }
288
289 - (void)testAllViewsAcceptExistingKeyHierarchies {
290 for(CKRecordZoneID* zoneID in self.ckksZones) {
291 [self putFakeKeyHierarchyInCloudKit:zoneID];
292 [self saveTLKMaterialToKeychain:zoneID];
293 [self expectCKKSTLKSelfShareUpload:zoneID];
294 }
295
296 [self.injectedManager.zoneChangeFetcher.fetchScheduler changeDelays:2*NSEC_PER_SEC continuingDelay:30*NSEC_PER_SEC];
297 self.silentFetchesAllowed = false;
298 [self expectCKFetch];
299
300 [self startCKKSSubsystem];
301 OCMVerifyAllWithDelay(self.mockDatabase, 20);
302
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);
305 }
306 }
307
308 - (void)testAddEngramManateeItems {
309 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
310
311 [self startCKKSSubsystem];
312
313 XCTestExpectation* engramChanged = [self expectChangeForView:self.engramZoneID.zoneName];
314 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
315 XCTestExpectation* manateeChanged = [self expectChangeForView:self.manateeZoneID.zoneName];
316
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];
320
321 OCMVerifyAllWithDelay(self.mockDatabase, 20);
322 [self waitForExpectations:@[engramChanged] timeout:1];
323 [self waitForExpectations:@[pcsChanged] timeout:1];
324
325 pcsChanged = [self expectChangeForView:@"PCS"];
326
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];
330
331 OCMVerifyAllWithDelay(self.mockDatabase, 20);
332 [self waitForExpectations:@[manateeChanged] timeout:1];
333 [self waitForExpectations:@[pcsChanged] timeout:1];
334 }
335
336 - (void)testAddAutoUnlockItems {
337 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
338
339 [self startCKKSSubsystem];
340
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;
345
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];
349
350 OCMVerifyAllWithDelay(self.mockDatabase, 20);
351 [self waitForExpectations:@[autoUnlockChanged] timeout:1];
352 [self waitForExpectations:@[pcsChanged] timeout:0.2];
353 }
354
355 - (void)testAddHealthItems {
356 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
357
358 [self startCKKSSubsystem];
359
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;
364
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];
368
369 OCMVerifyAllWithDelay(self.mockDatabase, 20);
370 [self waitForExpectations:@[healthChanged] timeout:1];
371 [self waitForExpectations:@[pcsChanged] timeout:0.2];
372 }
373
374 - (void)testAddApplePayItems {
375 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
376
377 [self startCKKSSubsystem];
378
379 XCTestExpectation* applepayChanged = [self expectChangeForView:self.applepayZoneID.zoneName];
380 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
381
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];
385
386 OCMVerifyAllWithDelay(self.mockDatabase, 20);
387 [self waitForExpectations:@[applepayChanged] timeout:1];
388 [self waitForExpectations:@[pcsChanged] timeout:0.2];
389 }
390
391 - (void)testAddHomeItems {
392 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
393
394 [self startCKKSSubsystem];
395
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"];
399
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];
403
404 OCMVerifyAllWithDelay(self.mockDatabase, 20);
405 [self waitForExpectations:@[homeChanged] timeout:1];
406 [self waitForExpectations:@[pcsChanged] timeout:0.2];
407 }
408
409 - (void)testAddLimitedPeersAllowedItems {
410 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
411
412 [self startCKKSSubsystem];
413
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"];
417
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];
421
422 OCMVerifyAllWithDelay(self.mockDatabase, 20);
423 [self waitForExpectations:@[limitedChanged] timeout:1];
424 [self waitForExpectations:@[pcsChanged] timeout:0.2];
425 }
426
427 - (void)testAddOtherViewHintItem {
428 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
429
430 [self startCKKSSubsystem];
431
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];
435
436 sleep(1);
437 OCMVerifyAllWithDelay(self.mockDatabase, 20);
438 }
439
440 - (void)testReceiveItemInView {
441 [self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
442 [self startCKKSSubsystem];
443
444 for(CKRecordZoneID* zoneID in self.ckksZones) {
445 [self expectCKKSTLKSelfShareUpload:zoneID];
446 }
447
448 [self waitForKeyHierarchyReadinesses];
449
450 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
451
452 CKRecord* ckr = [self createFakeRecord:self.engramZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
453 [self.engramZone addToZone: ckr];
454
455 XCTestExpectation* engramChanged = [self expectChangeForView:self.engramZoneID.zoneName];
456 XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
457
458 // Trigger a notification (with hilariously fake data)
459 [self.engramView notifyZoneChange:nil];
460
461 [self.engramView waitForFetchAndIncomingQueueProcessing];
462 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
463
464 [self waitForExpectations:@[engramChanged] timeout:1];
465 [self waitForExpectations:@[pcsChanged] timeout:1];
466 }
467
468 - (void)testRecoverFromCloudKitOldChangeTokenInKeyHierarchyFetch {
469 [self putFakeKeyHierachiesInCloudKit];
470 [self saveTLKsToKeychain];
471
472
473 [self expectCKKSTLKSelfShareUploads];
474
475 // Spin up CKKS subsystem.
476 [self startCKKSSubsystem];
477
478 [self waitForKeyHierarchyReadinesses];
479
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);
484
485 // Delete all old database states, to destroy the change tag validity
486 [self.manateeZone.pastDatabases removeAllObjects];
487
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
492
493 [self.manateeView dispatchSyncWithAccountKeys: ^bool {
494 [self.manateeView _onqueueKeyStateMachineRequestFetch];
495 return true;
496 }];
497
498 XCTAssertEqual(0, [self.manateeView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
499
500 [self.manateeView waitForFetchAndIncomingQueueProcessing];
501
502 OCMVerifyAllWithDelay(self.mockDatabase, 20);
503
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);
513 }
514
515 - (void)testResetAllCloudKitZones {
516 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
517 OCMExpect([self.suggestTLKUpload trigger]);
518
519 [self putFakeKeyHierachiesInCloudKit];
520 [self saveTLKsToKeychain];
521 [self expectCKKSTLKSelfShareUploads];
522
523 // Spin up CKKS subsystem.
524 [self startCKKSSubsystem];
525 [self waitForKeyHierarchyReadinesses];
526
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];
532
533 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
534
535 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
536
537 // CKKS should issue exactly one deletion for all of these
538 self.silentZoneDeletesAllowed = true;
539
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];
545 }];
546
547 // Sneak in and perform Octagon's duties
548 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
549 [self performOctagonTLKUpload:self.ckksViews];
550
551 [self waitForExpectations:@[resetExpectation] timeout:20];
552
553 OCMVerifyAllWithDelay(self.mockDatabase, 20);
554
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);
563 }
564
565 - (void)testResetAllCloudKitZonesWithPartialZonesMissing {
566 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
567 OCMExpect([self.suggestTLKUpload trigger]);
568
569 [self putFakeKeyHierachiesInCloudKit];
570 [self saveTLKsToKeychain];
571 [self expectCKKSTLKSelfShareUploads];
572
573 // Spin up CKKS subsystem.
574 [self startCKKSSubsystem];
575 [self waitForKeyHierarchyReadinesses];
576
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];
582
583 self.zones[self.manateeZoneID] = nil;
584 self.keys[self.manateeZoneID] = nil;
585 self.zones[self.applepayZoneID] = nil;
586 self.keys[self.applepayZoneID] = nil;
587
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"]];
590
591 // CKKS should issue exactly one deletion for all of these
592 self.silentZoneDeletesAllowed = true;
593
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];
599 }];
600
601 // Sneak in and perform Octagon's duties
602 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
603 [self performOctagonTLKUpload:self.ckksViews];
604
605 [self waitForExpectations:@[resetExpectation] timeout:20];
606
607 OCMVerifyAllWithDelay(self.mockDatabase, 20);
608
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);
617 }
618
619 - (void)testResetMultiCloudKitZoneCloudKitRejects {
620 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
621 OCMExpect([self.suggestTLKUpload trigger]);
622
623 [self putFakeKeyHierachiesInCloudKit];
624 [self saveTLKsToKeychain];
625 [self expectCKKSTLKSelfShareUploads];
626
627 // Spin up CKKS subsystem.
628 [self startCKKSSubsystem];
629 [self waitForKeyHierarchyReadinesses];
630
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];
636
637 self.nextModifyRecordZonesError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
638 code:CKErrorZoneBusy
639 userInfo:@{
640 CKErrorRetryAfterKey: @(0.2),
641 NSUnderlyingErrorKey: [[CKPrettyError alloc] initWithDomain:CKErrorDomain
642 code:2029
643 userInfo:nil],
644 }];
645 self.silentZoneDeletesAllowed = true;
646
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"]];
649
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];
655 }];
656
657 // Sneak in and perform Octagon's duties
658 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
659 [self performOctagonTLKUpload:self.ckksViews];
660
661 [self waitForExpectations:@[resetExpectation] timeout:20];
662
663 OCMVerifyAllWithDelay(self.mockDatabase, 20);
664
665 XCTAssertNil(self.nextModifyRecordZonesError, "zone modification error should have been cleared");
666 }
667
668 - (void)testMultiZoneDeviceStateUploadGood {
669 [self putFakeKeyHierachiesInCloudKit];
670 [self saveTLKsToKeychain];
671 [self expectCKKSTLKSelfShareUploads];
672
673 // Spin up CKKS subsystem.
674 [self startCKKSSubsystem];
675 [self waitForKeyHierarchyReadinesses];
676
677 for(CKKSKeychainView* view in self.ckksViews) {
678 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
679 deletedRecordTypeCounts:nil
680 zoneID:view.zoneID
681 checkModifiedRecord:nil
682 runAfterModification:nil];
683 }
684
685 [self.injectedManager xpc24HrNotification];
686
687 OCMVerifyAllWithDelay(self.mockDatabase, 20);
688 }
689
690 - (void)testMultiZoneResync {
691 // Set up
692 [self putFakeKeyHierachiesInCloudKit];
693 [self saveTLKsToKeychain];
694 [self expectCKKSTLKSelfShareUploads];
695
696 // Put sample data in zones, and save off a change token for later fetch shenanigans
697 [self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D00" withAccount:@"manatee0"]];
698 [self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D01" withAccount:@"manatee1"]];
699 CKServerChangeToken* manateeChangeToken1 = self.manateeZone.currentChangeToken;
700 [self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D02" withAccount:@"manatee2"]];
701 [self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D03" withAccount:@"manatee3"]];
702
703 [self.healthZone addToZone:[self createFakeRecord:self.healthZoneID recordName:@"7B598D31-0000-0000-FFFF-5A507ACB2D00" withAccount:@"health0"]];
704 [self.healthZone addToZone:[self createFakeRecord:self.healthZoneID recordName:@"7B598D31-0000-0000-FFFF-5A507ACB2D01" withAccount:@"health1"]];
705
706 [self startCKKSSubsystem];
707 [self waitForKeyHierarchyReadinesses];
708
709 OCMVerifyAllWithDelay(self.mockDatabase, 20);
710
711 [self.manateeView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
712 [self.healthView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
713
714 [self findGenericPassword:@"manatee0" expecting:errSecSuccess];
715 [self findGenericPassword:@"manatee1" expecting:errSecSuccess];
716 [self findGenericPassword:@"manatee2" expecting:errSecSuccess];
717 [self findGenericPassword:@"manatee3" expecting:errSecSuccess];
718 [self findGenericPassword:@"health0" expecting:errSecSuccess];
719 [self findGenericPassword:@"health1" expecting:errSecSuccess];
720
721 // Now, we resync. But, the manatee zone comes down in two fetches
722 self.silentFetchesAllowed = false;
723 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
724 // Assert that the fetch is a refetch
725 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.manateeZoneID].previousServerChangeToken;
726 if(changeToken == nil) {
727 return YES;
728 } else {
729 return NO;
730 }
731 } runBeforeFinished:^{}];
732 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
733 // Assert that the fetch is happening with the change token we paused at before
734 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.manateeZoneID].previousServerChangeToken;
735 if(changeToken && [changeToken isEqual:manateeChangeToken1]) {
736 return YES;
737 } else {
738 return NO;
739 }
740 } runBeforeFinished:^{}];
741
742 self.manateeZone.limitFetchTo = manateeChangeToken1;
743
744 // Attempt to trigger simultaneous key state resyncs. This is a horrible hack...
745 [self.manateeView dispatchSyncWithAccountKeys:^bool {
746 self.manateeView.keyStateFullRefetchRequested = YES;
747 [self.manateeView _onqueueAdvanceKeyStateMachineToState:nil withError:nil];
748 return true;
749 }];
750 [self.healthView dispatchSyncWithAccountKeys:^bool {
751 self.healthView.keyStateFullRefetchRequested = YES;
752 [self.healthView _onqueueAdvanceKeyStateMachineToState:nil withError:nil];
753 return true;
754 }];
755
756 OCMVerifyAllWithDelay(self.mockDatabase, 20);
757
758 [self.manateeView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
759 [self.healthView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
760
761 // And all items should still exist
762 [self findGenericPassword:@"manatee0" expecting:errSecSuccess];
763 [self findGenericPassword:@"manatee1" expecting:errSecSuccess];
764 [self findGenericPassword:@"manatee2" expecting:errSecSuccess];
765 [self findGenericPassword:@"manatee3" expecting:errSecSuccess];
766 [self findGenericPassword:@"health0" expecting:errSecSuccess];
767 [self findGenericPassword:@"health1" expecting:errSecSuccess];
768 }
769
770 @end
771
772 #endif // OCTAGON
773