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