]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTests+ForwardCompatibility.m
Security-59754.41.1.tar.gz
[apple/security.git] / keychain / ckks / tests / CKKSTests+ForwardCompatibility.m
1 /*
2 * Copyright (c) 2020 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #if OCTAGON
25
26 #import <CloudKit/CloudKit.h>
27 #import <XCTest/XCTest.h>
28 #import <OCMock/OCMock.h>
29
30 #import <TrustedPeers/TrustedPeers.h>
31 #import <TrustedPeers/TPPBPolicyKeyViewMapping.h>
32 #import <TrustedPeers/TPDictionaryMatchingRules.h>
33
34 #import "keychain/categories/NSError+UsefulConstructors.h"
35 #import "keychain/ckks/CKKS.h"
36 #import "keychain/ckks/CKKSIncomingQueueEntry.h"
37 #import "keychain/ckks/CKKSOutgoingQueueEntry.h"
38 #import "keychain/ckks/CloudKitCategories.h"
39 #import "keychain/ckks/tests/CKKSTests.h"
40 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
41 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
42 #import "keychain/ckks/tests/MockCloudKit.h"
43
44 @interface CloudKitKeychainForwardCompatibilityTests : CloudKitKeychainSyncingTestsBase
45 @property CKRecordZoneID* unknownZoneID;
46 @property CKRecordZoneID* passwordsZoneID;
47 @property CKKSKeychainView* passwordsView;
48
49 @property TPSyncingPolicy* originalPolicy;
50 @property TPSyncingPolicy* originalPolicyPlusUnknownVwht;
51 @property TPSyncingPolicy* allItemsToPasswordsPolicy;
52 @end
53
54 @implementation CloudKitKeychainForwardCompatibilityTests
55
56 - (void)setUp {
57 [super setUp];
58
59 self.passwordsZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Passwords" ownerName:CKCurrentUserDefaultName];
60 self.unknownZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"unknown-zone" ownerName:CKCurrentUserDefaultName];
61
62 self.zones[self.passwordsZoneID] = [[FakeCKZone alloc] initZone:self.passwordsZoneID];
63
64 self.originalPolicy = self.viewSortingPolicyForManagedViewList;
65
66
67 NSMutableArray<TPPBPolicyKeyViewMapping*>* newRules = [self.originalPolicy.keyViewMapping mutableCopy];
68 TPPBPolicyKeyViewMapping* unknownVwhtMapping = [[TPPBPolicyKeyViewMapping alloc] init];
69 unknownVwhtMapping.view = self.keychainZoneID.zoneName;
70 unknownVwhtMapping.matchingRule = [TPDictionaryMatchingRule fieldMatch:@"vwht"
71 fieldRegex:[NSString stringWithFormat:@"^%@$", self.unknownZoneID.zoneName]];
72 [newRules insertObject:unknownVwhtMapping atIndex:0];
73
74 self.originalPolicyPlusUnknownVwht = [[TPSyncingPolicy alloc] initWithModel:@"test-policy"
75 version:[[TPPolicyVersion alloc] initWithVersion:2 hash:@"fake-policy-for-views-with-unknown-view"]
76 viewList:self.originalPolicy.viewList
77 userControllableViews:self.originalPolicy.userControllableViews
78 syncUserControllableViews:self.originalPolicy.syncUserControllableViews
79 viewsToPiggybackTLKs:self.originalPolicy.viewsToPiggybackTLKs
80 keyViewMapping:newRules];
81
82 TPPBPolicyKeyViewMapping* passwordsVwhtMapping = [[TPPBPolicyKeyViewMapping alloc] init];
83 passwordsVwhtMapping.view = self.passwordsZoneID.zoneName;
84 passwordsVwhtMapping.matchingRule = [TPDictionaryMatchingRule trueMatch];
85
86 self.allItemsToPasswordsPolicy = [[TPSyncingPolicy alloc] initWithModel:@"test-policy"
87 version:[[TPPolicyVersion alloc] initWithVersion:2 hash:@"fake-policy-for-views-with-passwords-view"]
88 viewList:[NSSet setWithArray:@[self.keychainView.zoneName, self.passwordsZoneID.zoneName]]
89 userControllableViews:self.originalPolicy.userControllableViews
90 syncUserControllableViews:self.originalPolicy.syncUserControllableViews
91 viewsToPiggybackTLKs:self.originalPolicy.viewsToPiggybackTLKs
92 keyViewMapping:@[passwordsVwhtMapping]];
93 }
94
95 - (void)setPolicyAndWaitForQuiescence:(TPSyncingPolicy*)policy policyIsFresh:(BOOL)policyIsFresh {
96 [self.injectedManager setCurrentSyncingPolicy:policy policyIsFresh:policyIsFresh];
97 self.ckksViews = [NSMutableSet setWithArray:[[self.injectedManager views] allValues]];
98 [self beginSOSTrustedOperationForAllViews];
99
100 // And wait for everything to enter a resting state
101 for(CKKSKeychainView* view in self.ckksViews) {
102 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
103 [view waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
104 [view waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
105 }
106 }
107
108 - (void)testReceiveItemForWrongView {
109 self.requestPolicyCheck = OCMClassMock([CKKSNearFutureScheduler class]);
110 OCMExpect([self.requestPolicyCheck trigger]);
111
112 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
113
114 [self startCKKSSubsystem];
115 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
116
117 NSString* wrongZoneAccount = @"wrong-zone";
118 NSDictionary* item = [self fakeRecordDictionary:wrongZoneAccount zoneID:self.unknownZoneID];
119 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" zoneID:self.keychainZoneID];
120 CKRecord* ckr = [self newRecord:ckrid withNewItemData:item];
121 [self.keychainZone addToZone:ckr];
122
123 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
124 [self.keychainView waitForFetchAndIncomingQueueProcessing];
125
126 [self findGenericPassword:wrongZoneAccount expecting:errSecItemNotFound];
127
128 OCMVerifyAllWithDelay(self.requestPolicyCheck, 10);
129
130 NSError* zoneError = nil;
131 NSInteger count = [CKKSIncomingQueueEntry countByState:SecCKKSStateMismatchedView zone:self.keychainView.zoneID error:&zoneError];
132 XCTAssertNil(zoneError, "should be no error counting all IQEs");
133 XCTAssertEqual(count, 1, "Should be one mismatched IQE");
134 }
135
136 - (void)testConflictingItemInWrongView {
137 self.requestPolicyCheck = OCMClassMock([CKKSNearFutureScheduler class]);
138 OCMExpect([self.requestPolicyCheck trigger]);
139
140 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
141 [self createAndSaveFakeKeyHierarchy:self.passwordsZoneID];
142
143 [self startCKKSSubsystem];
144
145 [self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:NO];
146 self.passwordsView = [self.injectedManager findView:@"Passwords"];
147 XCTAssertNotNil(self.passwordsView, @"Policy created a passwords view");
148
149 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
150 XCTAssertEqual(0, [self.passwordsView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
151
152 NSDictionary* item = [self fakeRecordDictionary:@"account-delete-me" zoneID:self.passwordsZoneID];
153 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" zoneID:self.passwordsZoneID];
154 CKRecordID* ckr2id = [[CKRecordID alloc] initWithRecordName:@"FFFF8D31-F9C5-481E-98AC-5A507ACB2D85" zoneID:self.keychainZoneID];
155 CKRecord* ckr = [self newRecord:ckrid withNewItemData:item];
156
157 NSMutableDictionary* item2 = [item mutableCopy];
158 item2[@"v_Data"] = @"wrongview";
159 CKRecord* ckr2 = [self newRecord:ckr2id withNewItemData:item2];
160
161 // Receive the passwords item first
162 [self.zones[self.passwordsZoneID] addToZone:ckr];
163
164 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
165 [self.passwordsView waitForFetchAndIncomingQueueProcessing];
166 [self checkGenericPassword:@"data" account:@"account-delete-me"];
167
168 [self.zones[self.keychainZoneID] addToZone:ckr2];
169 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
170 [self.keychainView waitForFetchAndIncomingQueueProcessing];
171
172 // THe view should ask for an update, and receive one
173 OCMVerifyAllWithDelay(self.requestPolicyCheck, 10);
174 [self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:YES];
175
176 // And we have ignored the change in the other view
177 [self checkGenericPassword:@"data" account:@"account-delete-me"];
178
179 OCMVerifyAllWithDelay(self.mockDatabase, 20);
180
181 // And the item is then deleted
182 [self.zones[self.keychainZoneID] deleteCKRecordIDFromZone:ckr2id];
183 OCMExpect([self.requestPolicyCheck trigger]);
184 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
185 [self.keychainView waitForFetchAndIncomingQueueProcessing];
186 [self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:YES];
187
188 // The password should still exist
189 [self checkGenericPassword:@"data" account:@"account-delete-me"];
190 }
191
192 - (void)testConflictingItemInWrongViewWithLowerUUID {
193 self.requestPolicyCheck = OCMClassMock([CKKSNearFutureScheduler class]);
194 OCMExpect([self.requestPolicyCheck trigger]);
195
196 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
197 [self createAndSaveFakeKeyHierarchy:self.passwordsZoneID];
198
199 [self startCKKSSubsystem];
200
201 [self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:NO];
202 self.passwordsView = [self.injectedManager findView:@"Passwords"];
203 XCTAssertNotNil(self.passwordsView, @"Policy created a passwords view");
204
205 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
206 XCTAssertEqual(0, [self.passwordsView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
207
208 NSDictionary* item = [self fakeRecordDictionary:@"account-delete-me" zoneID:self.passwordsZoneID];
209 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" zoneID:self.passwordsZoneID];
210 CKRecordID* ckr2id = [[CKRecordID alloc] initWithRecordName:@"00008D31-F9C5-481E-98AC-5A507ACB2D85" zoneID:self.keychainZoneID];
211 CKRecord* ckr = [self newRecord:ckrid withNewItemData:item];
212
213 NSMutableDictionary* item2 = [item mutableCopy];
214 item2[@"v_Data"] = @"wrongview";
215 CKRecord* ckr2 = [self newRecord:ckr2id withNewItemData:item2];
216
217 // Receive the passwords item first
218 [self.zones[self.passwordsZoneID] addToZone:ckr];
219
220 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
221 [self.passwordsView waitForFetchAndIncomingQueueProcessing];
222 [self checkGenericPassword:@"data" account:@"account-delete-me"];
223
224 [self.zones[self.keychainZoneID] addToZone:ckr2];
225 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
226 [self.keychainView waitForFetchAndIncomingQueueProcessing];
227
228 // THe view should ask for an update, and receive one
229 OCMVerifyAllWithDelay(self.requestPolicyCheck, 10);
230 [self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:YES];
231
232 OCMVerifyAllWithDelay(self.mockDatabase, 20);
233 [self checkGenericPassword:@"data" account:@"account-delete-me"];
234
235 // And the item is then deleted
236 [self.zones[self.keychainZoneID] deleteCKRecordIDFromZone:ckr2id];
237 OCMExpect([self.requestPolicyCheck trigger]);
238 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
239 [self.keychainView waitForFetchAndIncomingQueueProcessing];
240 [self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:YES];
241
242 // The password should still exist
243 [self checkGenericPassword:@"data" account:@"account-delete-me"];
244 }
245
246 - (void)testConflictingItemInWrongViewWithSameUUID {
247 self.requestPolicyCheck = OCMClassMock([CKKSNearFutureScheduler class]);
248 OCMExpect([self.requestPolicyCheck trigger]);
249
250 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
251 [self createAndSaveFakeKeyHierarchy:self.passwordsZoneID];
252
253 [self startCKKSSubsystem];
254
255 [self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:NO];
256 self.passwordsView = [self.injectedManager findView:@"Passwords"];
257 XCTAssertNotNil(self.passwordsView, @"Policy created a passwords view");
258
259 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
260 XCTAssertEqual(0, [self.passwordsView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
261
262 NSDictionary* item = [self fakeRecordDictionary:@"account-delete-me" zoneID:self.passwordsZoneID];
263 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" zoneID:self.passwordsZoneID];
264 CKRecordID* ckr2id = [[CKRecordID alloc] initWithRecordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" zoneID:self.keychainZoneID];
265 CKRecord* ckr = [self newRecord:ckrid withNewItemData:item];
266
267 NSMutableDictionary* item2 = [item mutableCopy];
268 item2[@"v_Data"] = @"wrongview";
269 CKRecord* ckr2 = [self newRecord:ckr2id withNewItemData:item2];
270
271 // Receive the passwords item first
272 [self.zones[self.passwordsZoneID] addToZone:ckr];
273
274 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
275 [self.passwordsView waitForFetchAndIncomingQueueProcessing];
276 [self checkGenericPassword:@"data" account:@"account-delete-me"];
277
278 [self.zones[self.keychainZoneID] addToZone:ckr2];
279 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
280 [self.keychainView waitForFetchAndIncomingQueueProcessing];
281
282 // THe view should ask for a policy update, and receive one
283 OCMVerifyAllWithDelay(self.requestPolicyCheck, 10);
284 [self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:YES];
285
286 OCMVerifyAllWithDelay(self.mockDatabase, 20);
287 [self checkGenericPassword:@"data" account:@"account-delete-me"];
288
289 // And the item is then deleted
290 [self.zones[self.keychainZoneID] deleteCKRecordIDFromZone:ckr2id];
291 OCMExpect([self.requestPolicyCheck trigger]);
292 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
293 [self.keychainView waitForFetchAndIncomingQueueProcessing];
294 [self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:YES];
295
296 // The password should still exist
297 [self checkGenericPassword:@"data" account:@"account-delete-me"];
298 }
299
300 - (void)testReceiveItemForFuturePolicy {
301 self.requestPolicyCheck = OCMClassMock([CKKSNearFutureScheduler class]);
302 OCMExpect([self.requestPolicyCheck trigger]);
303
304 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
305
306 [self startCKKSSubsystem];
307 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
308
309 NSString* wrongZoneAccount = @"wrong-zone";
310 NSDictionary* item = [self fakeRecordDictionary:wrongZoneAccount zoneID:self.unknownZoneID];
311 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" zoneID:self.keychainZoneID];
312 CKRecord* ckr = [self newRecord:ckrid withNewItemData:item];
313 [self.keychainZone addToZone:ckr];
314
315 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
316 [self.keychainView waitForFetchAndIncomingQueueProcessing];
317
318 [self findGenericPassword:wrongZoneAccount expecting:errSecItemNotFound];
319
320 OCMVerifyAllWithDelay(self.requestPolicyCheck, 10);
321
322 // Now, Octagon discovers that there's a new policy that allows this item in the keychain view
323 TPSyncingPolicy* currentPolicy = self.injectedManager.policy;
324 XCTAssertNotNil(currentPolicy, "should have a current policy");
325
326 [self setPolicyAndWaitForQuiescence:self.originalPolicyPlusUnknownVwht policyIsFresh:YES];
327
328 [self findGenericPassword:wrongZoneAccount expecting:errSecSuccess];
329 }
330
331 - (void)testHandleItemMovedBetweenViewsBeforePolicyChange {
332 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
333 [self createAndSaveFakeKeyHierarchy:self.passwordsZoneID];
334
335 [self startCKKSSubsystem];
336 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
337
338 // A password is created and uploaded out of the keychain view.
339 __block CKRecord* itemCKRecord = nil;
340 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:^BOOL(CKRecord * _Nonnull record) {
341 itemCKRecord = record;
342 return YES;
343 }];
344 NSString* itemAccount = @"account-delete-me";
345 [self addGenericPassword:@"data" account:itemAccount viewHint:self.keychainZoneID.zoneName];
346 OCMVerifyAllWithDelay(self.mockDatabase, 20);
347 XCTAssertNotNil(itemCKRecord, "Should have some CKRecord for the added item");
348
349 // Update etag as well
350 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
351 [self.keychainView waitForFetchAndIncomingQueueProcessing];
352
353 // Another device shows up, changes the item sync policy, and moves the item over into the passwords view.
354
355 NSDictionary* itemContents = [self decryptRecord:itemCKRecord];
356 XCTAssertNotNil(itemContents, "should have some item contents");
357
358 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:itemCKRecord.recordID.recordName zoneID:self.passwordsZoneID];
359 CKRecord* ckr = [self newRecord:ckrid withNewItemData:itemContents];
360 [self.zones[self.passwordsZoneID] addToZone:ckr];
361
362 TPSyncingPolicy* currentPolicy = self.injectedManager.policy;
363 XCTAssertNotNil(currentPolicy, "should have a current policy");
364
365 // In this test, we receive the deletion, and then the policy change adding the Passwords view
366 [self.keychainZone deleteCKRecordIDFromZone:itemCKRecord.recordID];
367 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
368 [self.keychainView waitForFetchAndIncomingQueueProcessing];
369 [self findGenericPassword:itemAccount expecting:errSecItemNotFound];
370
371 [self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:NO];
372
373 // And once passwords syncs, the item should appear again
374 [self findGenericPassword:itemAccount expecting:errSecSuccess];
375 }
376
377 - (void)testHandleItemMovedBetweenViewsAfterPolicyChange {
378 self.requestPolicyCheck = OCMClassMock([CKKSNearFutureScheduler class]);
379 OCMExpect([self.requestPolicyCheck trigger]);
380
381 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
382 [self createAndSaveFakeKeyHierarchy:self.passwordsZoneID];
383
384 [self startCKKSSubsystem];
385 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
386
387 // A password is created and uploaded out of the keychain view.
388 __block CKRecord* itemCKRecord = nil;
389 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:^BOOL(CKRecord * _Nonnull record) {
390 itemCKRecord = record;
391 return YES;
392 }];
393 NSString* itemAccount = @"account-delete-me";
394 [self addGenericPassword:@"data" account:itemAccount viewHint:self.keychainZoneID.zoneName];
395 OCMVerifyAllWithDelay(self.mockDatabase, 20);
396 XCTAssertNotNil(itemCKRecord, "Should have some CKRecord for the added item");
397
398 // Update etag as well
399 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
400 [self.keychainView waitForFetchAndIncomingQueueProcessing];
401
402 // Another device shows up, changes the item sync policy, and moves the item over into the Passwords view.
403 // But, in this case, we receive the item delete in the Keychain view after we've synced the item in the Passwords view
404
405 NSDictionary* itemContents = [self decryptRecord:itemCKRecord];
406 XCTAssertNotNil(itemContents, "should have some item contents");
407
408 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:itemCKRecord.recordID.recordName zoneID:self.passwordsZoneID];
409 CKRecord* ckr = [self newRecord:ckrid withNewItemData:itemContents];
410 [self.zones[self.passwordsZoneID] addToZone:ckr];
411
412 TPSyncingPolicy* currentPolicy = self.injectedManager.policy;
413 XCTAssertNotNil(currentPolicy, "should have a current policy");
414
415 [self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:NO];
416
417 CKKSKeychainView* passwordsView = [self.injectedManager findView:self.passwordsZoneID.zoneName];
418 XCTAssertNotNil(passwordsView, @"Should have a passwords view");
419
420 // And once Passwords syncs, the item should appear again
421 [self findGenericPassword:itemAccount expecting:errSecSuccess];
422
423 // And now we receive the delete in the keychain view. The item should still exist!
424 [self.keychainZone deleteCKRecordIDFromZone:itemCKRecord.recordID];
425 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
426 [self.keychainView waitForFetchAndIncomingQueueProcessing];
427 [self findGenericPassword:itemAccount expecting:errSecSuccess];
428
429 // Keychain View should have asked for a policy set
430 OCMVerifyAllWithDelay(self.requestPolicyCheck, 10);
431 [self.injectedManager setCurrentSyncingPolicy:self.allItemsToPasswordsPolicy policyIsFresh:YES];
432
433 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
434 [self.keychainView waitForFetchAndIncomingQueueProcessing];
435 [self findGenericPassword:itemAccount expecting:errSecSuccess];
436
437 NSError* zoneError = nil;
438 NSInteger count = [CKKSIncomingQueueEntry countByState:SecCKKSStateMismatchedView zone:self.keychainView.zoneID error:&zoneError];
439 XCTAssertNil(zoneError, "should be no error counting all IQEs");
440 XCTAssertEqual(count, 0, "Should be no remaining mismatched IQEs");
441 }
442
443 - (void)testMoveItemUploadedToOldZone {
444 self.requestPolicyCheck = OCMClassMock([CKKSNearFutureScheduler class]);
445 OCMExpect([self.requestPolicyCheck trigger]);
446
447 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
448 [self createAndSaveFakeKeyHierarchy:self.passwordsZoneID];
449
450 [self startCKKSSubsystem];
451 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
452
453 [self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:NO];
454
455 // Now, someone uploads an item to the keychain view. We should move it to the passwords view
456 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
457 [self.keychainZone addToZone:ckr];
458
459 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
460 [self.keychainView waitForFetchAndIncomingQueueProcessing];
461
462 // The keychain view should request a policy refetch
463 OCMVerifyAllWithDelay(self.requestPolicyCheck, 10);
464
465 // Note that ideally, we'd remove the old item from CloudKit. But, other devices which participate in CKKS4All
466 // might not have this forward-compatiblity change, and will treat this as a deletion. If they process this deletion,
467 // then sync the resulting tombstone to a third SOS device, then receive the addition in the 'right' view, and then the
468 // tombstone syncs back to the CKKS4All devices, then we might end up deleting the item across the account.
469 // Until enough internal folk have moved onto builds with this forward-compat change, we can't issue these deletes.
470 //[self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
471
472 // The item should be reuploaded to Passwords, though.
473 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.passwordsZoneID checkItem:^BOOL(CKRecord * _Nonnull record) {
474 return [record.recordID.recordName isEqualToString:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
475 }];
476
477 [self.injectedManager setCurrentSyncingPolicy:self.allItemsToPasswordsPolicy policyIsFresh:YES];
478 OCMVerifyAllWithDelay(self.mockDatabase, 20);
479
480 // And the keychain item should still exist
481 [self findGenericPassword:@"account0" expecting:errSecSuccess];
482 }
483
484 @end
485
486 #endif // OCTAGON