]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSFetchTests.m
Security-59306.140.5.tar.gz
[apple/security.git] / keychain / ckks / tests / CKKSFetchTests.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 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
10 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
11 #import "keychain/ckks/tests/CloudKitKeychainSyncingTestsBase.h"
12 #import "keychain/ckks/CKKS.h"
13 #import "keychain/ckks/CKKSKeychainView.h"
14 #import "keychain/ckks/CKKSZoneStateEntry.h"
15
16 #import "keychain/ckks/tests/MockCloudKit.h"
17 #import "keychain/ot/ObjCImprovements.h"
18
19 @interface CloudKitKeychainFetchTests : CloudKitKeychainSyncingTestsBase
20 @end
21
22 @implementation CloudKitKeychainFetchTests
23
24 - (void)testMoreComing {
25 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
26 [self saveTLKMaterialToKeychain:self.keychainZoneID];
27
28 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
29 [self startCKKSSubsystem];
30 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
31 OCMVerifyAllWithDelay(self.mockDatabase, 20);
32 [self waitForCKModifications];
33
34 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
35
36 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D00" withAccount:@"account0"]];
37 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D01" withAccount:@"account1"]];
38 CKServerChangeToken* ck1 = ckzone.currentChangeToken;
39 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D02" withAccount:@"account2"]];
40 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D03" withAccount:@"account3"]];
41
42 ckzone.limitFetchTo = ck1;
43
44 self.silentFetchesAllowed = false;
45 [self expectCKFetch];
46 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
47 // Assert that the fetch is happening with the change token we paused at before
48 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.keychainZoneID].previousServerChangeToken;
49 if(changeToken && [changeToken isEqual:ck1]) {
50 return YES;
51 } else {
52 return NO;
53 }
54 } runBeforeFinished:^{}];
55
56 // Trigger a notification (with hilariously fake data)
57 [self.keychainView notifyZoneChange:nil];
58
59 [self.keychainView waitForFetchAndIncomingQueueProcessing];
60
61 OCMVerifyAllWithDelay(self.mockDatabase, 20);
62
63 NSTimeInterval delta = [ckzone.fetchRecordZoneChangesTimestamps[2] timeIntervalSinceDate:ckzone.fetchRecordZoneChangesTimestamps[1]];
64 XCTAssertLessThan(delta, 2, "operation 1 and 2 should be back-to-back");
65
66 [self findGenericPassword: @"account0" expecting:errSecSuccess];
67 [self findGenericPassword: @"account1" expecting:errSecSuccess];
68 [self findGenericPassword: @"account2" expecting:errSecSuccess];
69 [self findGenericPassword: @"account3" expecting:errSecSuccess];
70 }
71
72 - (void)testMoreComingRepeated {
73 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
74 [self saveTLKMaterialToKeychain:self.keychainZoneID];
75
76 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
77 [self startCKKSSubsystem];
78 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
79 OCMVerifyAllWithDelay(self.mockDatabase, 20);
80 [self waitForCKModifications];
81
82 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
83
84 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D00" withAccount:@"account0"]];
85 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D01" withAccount:@"account1"]];
86 CKServerChangeToken* ck1 = ckzone.currentChangeToken;
87 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D02" withAccount:@"account2"]];
88 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D03" withAccount:@"account3"]];
89 CKServerChangeToken* ck2 = ckzone.currentChangeToken;
90 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D04" withAccount:@"account4"]];
91 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D05" withAccount:@"account5"]];
92
93 ckzone.limitFetchTo = ck1;
94
95 self.silentFetchesAllowed = false;
96 // This fetch will return up to ck1.
97 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
98 return YES;
99 } runBeforeFinished:^{
100 ckzone.limitFetchTo = ck2;
101 }];
102
103 // This fetch will return up to ck2.
104 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
105 // Assert that the fetch is happening with the change token we paused at before
106 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.keychainZoneID].previousServerChangeToken;
107 if(changeToken && [changeToken isEqual:ck1]) {
108 return YES;
109 } else {
110 return NO;
111 }
112 } runBeforeFinished:^{}];
113
114 // This fetch will return the final two items.
115 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
116 // Assert that the fetch is happening with the change token we paused at before
117 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.keychainZoneID].previousServerChangeToken;
118 if(changeToken && [changeToken isEqual:ck2]) {
119 return YES;
120 } else {
121 return NO;
122 }
123 } runBeforeFinished:^{}];
124
125 // Trigger a notification (with hilariously fake data)
126 [self.keychainView notifyZoneChange:nil];
127
128 [self.keychainView waitForFetchAndIncomingQueueProcessing];
129
130 OCMVerifyAllWithDelay(self.mockDatabase, 20);
131
132 NSTimeInterval delta = [ckzone.fetchRecordZoneChangesTimestamps[2] timeIntervalSinceDate:ckzone.fetchRecordZoneChangesTimestamps[1]];
133 XCTAssertLessThan(delta, 2, "operation 1 and 2 should be back-to-back");
134
135 delta = [ckzone.fetchRecordZoneChangesTimestamps[3] timeIntervalSinceDate:ckzone.fetchRecordZoneChangesTimestamps[2]];
136 XCTAssertLessThan(delta, 2, "operation 2 and 3 should be back-to-back");
137
138 [self findGenericPassword: @"account0" expecting:errSecSuccess];
139 [self findGenericPassword: @"account1" expecting:errSecSuccess];
140 [self findGenericPassword: @"account2" expecting:errSecSuccess];
141 [self findGenericPassword: @"account3" expecting:errSecSuccess];
142 [self findGenericPassword: @"account4" expecting:errSecSuccess];
143 [self findGenericPassword: @"account5" expecting:errSecSuccess];
144 }
145
146 - (void)testMoreComingDespitePartialTimeout {
147 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
148 [self saveTLKMaterialToKeychain:self.keychainZoneID];
149
150 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
151 [self startCKKSSubsystem];
152 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
153 OCMVerifyAllWithDelay(self.mockDatabase, 20);
154 [self waitForCKModifications];
155
156 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
157
158 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D00" withAccount:@"account0"]];
159 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D01" withAccount:@"account1"]];
160 CKServerChangeToken* ck1 = ckzone.currentChangeToken;
161 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D02" withAccount:@"account2"]];
162 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D03" withAccount:@"account3"]];
163
164 // The fetch fails with partial results
165 ckzone.limitFetchTo = ck1;
166 ckzone.limitFetchError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkFailure userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:4]}];
167
168 self.silentFetchesAllowed = false;
169 [self expectCKFetch];
170 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
171 // Assert that the fetch is happening with the change token we paused at before
172 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.keychainZoneID].previousServerChangeToken;
173 if(changeToken && [changeToken isEqual:ck1]) {
174 return YES;
175 } else {
176 return NO;
177 }
178 } runBeforeFinished:^{}];
179
180 // Trigger a notification (with hilariously fake data)
181 [self.keychainView notifyZoneChange:nil];
182
183 [self.keychainView waitForFetchAndIncomingQueueProcessing];
184
185 OCMVerifyAllWithDelay(self.mockDatabase, 20);
186
187 [self findGenericPassword: @"account0" expecting:errSecSuccess];
188 [self findGenericPassword: @"account1" expecting:errSecSuccess];
189 [self findGenericPassword: @"account2" expecting:errSecSuccess];
190 [self findGenericPassword: @"account3" expecting:errSecSuccess];
191 }
192
193 - (void)testMoreComingWithFullFailure {
194 WEAKIFY(self);
195 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
196 [self saveTLKMaterialToKeychain:self.keychainZoneID];
197
198 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
199 [self startCKKSSubsystem];
200 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
201 OCMVerifyAllWithDelay(self.mockDatabase, 20);
202 [self waitForCKModifications];
203
204 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
205
206 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D00" withAccount:@"account0"]];
207 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D01" withAccount:@"account1"]];
208 CKServerChangeToken* ck1 = ckzone.currentChangeToken;
209 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D02" withAccount:@"account2"]];
210 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D03" withAccount:@"account3"]];
211
212 // The fetch fails with partial results
213 ckzone.limitFetchTo = ck1;
214 ckzone.limitFetchError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkFailure userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:4]}];
215
216 self.silentFetchesAllowed = false;
217 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
218 return YES;
219 } runBeforeFinished:^{
220 STRONGIFY(self);
221 // We want to fail with a full network failure error, but we explicitly don't want to set network reachability:
222 // CKKS won't send the fetch in that case. So...
223 [self.keychainZone failNextFetchWith:[NSError errorWithDomain:CKErrorDomain code:CKErrorNetworkFailure userInfo:NULL]];
224 [self expectCKFetch];
225 }];
226
227 // Trigger a notification (with hilariously fake data)
228 [self.keychainView notifyZoneChange:nil];
229
230 // Wait for both fetches....
231 OCMVerifyAllWithDelay(self.mockDatabase, 20);
232 OCMVerifyAllWithDelay(self.mockDatabase, 20);
233
234 // Potential race here: we need to start this expectation before CKKS issues the fetch. With a 4s delay, this should be safe.
235
236 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
237 // Assert that the fetch is happening with the change token we paused at before
238 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.keychainZoneID].previousServerChangeToken;
239 if(changeToken && [changeToken isEqual:ck1]) {
240 return YES;
241 } else {
242 return NO;
243 }
244 } runBeforeFinished:^{}];
245
246 [self.reachabilityTracker setNetworkReachability:true];
247
248 OCMVerifyAllWithDelay(self.mockDatabase, 20);
249
250 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
251
252 [self findGenericPassword: @"account0" expecting:errSecSuccess];
253 [self findGenericPassword: @"account1" expecting:errSecSuccess];
254 [self findGenericPassword: @"account2" expecting:errSecSuccess];
255 [self findGenericPassword: @"account3" expecting:errSecSuccess];
256 }
257
258 - (void)testFetchOnRestartWithMoreComing {
259 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
260 [self saveTLKMaterialToKeychain:self.keychainZoneID];
261
262 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
263 [self startCKKSSubsystem];
264 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
265 OCMVerifyAllWithDelay(self.mockDatabase, 20);
266 [self waitForCKModifications];
267
268 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
269
270 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D00" withAccount:@"account0"]];
271 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D01" withAccount:@"account1"]];
272 CKServerChangeToken* ck1 = ckzone.currentChangeToken;
273
274 // Allow CKKS to fetch fully, then fake on-disk that it received MoreComing.
275 // (It's very hard to tear down the retry logic in-process)
276 self.silentFetchesAllowed = false;
277 [self expectCKFetch];
278
279 // Trigger a notification (with hilariously fake data)
280 [self.keychainView notifyZoneChange:nil];
281
282 OCMVerifyAllWithDelay(self.mockDatabase, 20);
283 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
284 [self findGenericPassword: @"account0" expecting:errSecSuccess];
285 [self findGenericPassword: @"account1" expecting:errSecSuccess];
286
287 // Now, edit the on-disk CKSE
288 [self.keychainView halt];
289
290 [self.keychainView dispatchSync: ^bool {
291 NSError* error = nil;
292 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
293
294 XCTAssertNil(error, "no error pulling ckse from database");
295 XCTAssertNotNil(ckse, "received a ckse");
296
297 ckse.moreRecordsInCloudKit = YES;
298 [ckse saveToDatabase: &error];
299 XCTAssertNil(error, "no error saving to database");
300 return true;
301 }];
302
303 // CKKS should, upon restart, kick off a fetch starting from the previous checkpoint
304 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D02" withAccount:@"account2"]];
305 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D03" withAccount:@"account3"]];
306
307 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
308 // Assert that the fetch is happening with the change token we paused at before
309 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.keychainZoneID].previousServerChangeToken;
310 if(changeToken && [changeToken isEqual:ck1]) {
311 return YES;
312 } else {
313 return NO;
314 }
315 } runBeforeFinished:^{}];
316
317 self.keychainView = [self.injectedManager restartZone:self.keychainZoneID.zoneName];
318 [self.keychainView beginCloudKitOperation];
319 [self beginSOSTrustedViewOperation:self.keychainView];
320
321 OCMVerifyAllWithDelay(self.mockDatabase, 20);
322
323 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
324
325 [self findGenericPassword: @"account0" expecting:errSecSuccess];
326 [self findGenericPassword: @"account1" expecting:errSecSuccess];
327 [self findGenericPassword: @"account2" expecting:errSecSuccess];
328 [self findGenericPassword: @"account3" expecting:errSecSuccess];
329 }
330
331 - (void)testMoreComingAsFirstFetch {
332 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
333 [self saveTLKMaterialToKeychain:self.keychainZoneID];
334
335 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
336
337 [self.keychainZone addToZone: [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D00" withAccount:@"account0"]];
338 [self.keychainZone addToZone: [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D01" withAccount:@"account1"]];
339 CKServerChangeToken* ck1 = ckzone.currentChangeToken;
340 [self.keychainZone addToZone: [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D02" withAccount:@"account2"]];
341 [self.keychainZone addToZone: [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D03" withAccount:@"account3"]];
342
343 ckzone.limitFetchTo = ck1;
344
345 self.silentFetchesAllowed = false;
346 [self expectCKFetch];
347 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
348 // Assert that the fetch is happening with the change token we paused at before
349 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.keychainZoneID].previousServerChangeToken;
350 if(changeToken && [changeToken isEqual:ck1]) {
351 return YES;
352 } else {
353 return NO;
354 }
355 } runBeforeFinished:^{}];
356
357 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
358 [self startCKKSSubsystem];
359 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
360 OCMVerifyAllWithDelay(self.mockDatabase, 20);
361 [self waitForCKModifications];
362
363 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
364
365 OCMVerifyAllWithDelay(self.mockDatabase, 20);
366
367 [self findGenericPassword:@"account0" expecting:errSecSuccess];
368 [self findGenericPassword:@"account1" expecting:errSecSuccess];
369 [self findGenericPassword:@"account2" expecting:errSecSuccess];
370 [self findGenericPassword:@"account3" expecting:errSecSuccess];
371 }
372
373
374 @end
375
376 #endif // OCTAGON
377