]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTests.m
Security-58286.270.3.0.1.tar.gz
[apple/security.git] / keychain / ckks / tests / CKKSTests.m
1 /*
2 * Copyright (c) 2016 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 "CKKSTests.h"
27 #import <CloudKit/CloudKit.h>
28 #import <Foundation/NSXPCConnection_Private.h>
29 #import <XCTest/XCTest.h>
30 #import <OCMock/OCMock.h>
31
32 #include <Security/SecItemPriv.h>
33 #include <securityd/SecItemDb.h>
34 #include <securityd/SecItemServer.h>
35 #include <utilities/SecFileLocations.h>
36 #include <Security/SecureObjectSync/SOSInternal.h>
37
38 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
39 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
40 #import "keychain/ckks/CKKS.h"
41 #import "keychain/ckks/CKKSControlProtocol.h"
42 #import "keychain/ckks/CKKSCurrentKeyPointer.h"
43 #import "keychain/ckks/CKKSItemEncrypter.h"
44 #import "keychain/ckks/CKKSKey.h"
45 #import "keychain/ckks/CKKSOutgoingQueueEntry.h"
46 #import "keychain/ckks/CKKSIncomingQueueEntry.h"
47 #import "keychain/ckks/CKKSSynchronizeOperation.h"
48 #import "keychain/ckks/CKKSViewManager.h"
49 #import "keychain/ckks/CKKSZoneStateEntry.h"
50 #import "keychain/ckks/CKKSManifest.h"
51 #import "keychain/ckks/CKKSAnalytics.h"
52 #import "keychain/ckks/CKKSHealKeyHierarchyOperation.h"
53 #import "keychain/ckks/CKKSZoneChangeFetcher.h"
54 #import "keychain/categories/NSError+UsefulConstructors.h"
55
56 #import "keychain/ckks/tests/MockCloudKit.h"
57
58 #import "keychain/ckks/tests/CKKSTests.h"
59
60 // break abstraction
61 @interface CKKSLockStateTracker ()
62 @property (nullable) NSDate* lastUnlockedTime;
63 @end
64
65 @implementation CloudKitKeychainSyncingTests
66
67 #pragma mark - Tests
68
69 - (void)testBringupToKeyStateReady {
70 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
71 [self startCKKSSubsystem];
72
73 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
74 }
75
76 - (void)testAddItem {
77 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
78
79 // We expect a single record to be uploaded.
80 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
81
82 [self startCKKSSubsystem];
83 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
84
85 [self addGenericPassword: @"data" account: @"account-delete-me"];
86
87 OCMVerifyAllWithDelay(self.mockDatabase, 20);
88 }
89
90 - (void)testActiveTLKS {
91 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
92
93 // We expect a single record to be uploaded.
94 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
95
96 [self startCKKSSubsystem];
97
98 [self addGenericPassword: @"data" account: @"account-delete-me"];
99
100 OCMVerifyAllWithDelay(self.mockDatabase, 20);
101
102 NSDictionary<NSString *,NSString *>* tlks = [[CKKSViewManager manager] activeTLKs];
103
104 XCTAssertEqual([tlks count], (NSUInteger)1, "One TLK");
105 XCTAssertNotNil(tlks[@"keychain"], "keychain have a UUID");
106 }
107
108
109 - (void)testAddMultipleItems {
110 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
111 [self startCKKSSubsystem];
112
113 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
114 [self addGenericPassword: @"data" account: @"account-delete-me"];
115 OCMVerifyAllWithDelay(self.mockDatabase, 20);
116
117 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
118 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
119 OCMVerifyAllWithDelay(self.mockDatabase, 20);
120
121 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
122 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
123 OCMVerifyAllWithDelay(self.mockDatabase, 20);
124 }
125
126 - (void)testAddItemWithoutUUID {
127 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
128 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
129 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
130 [self saveTLKMaterialToKeychain:self.keychainZoneID];
131
132 [self startCKKSSubsystem];
133
134 [self.keychainView waitUntilAllOperationsAreFinished];
135
136 SecCKKSTestSetDisableAutomaticUUID(true);
137 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
138
139 SecCKKSTestSetDisableAutomaticUUID(false);
140
141 // We then expect an upload of the added item
142 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
143
144 OCMVerifyAllWithDelay(self.mockDatabase, 20);
145 }
146
147 - (void)testModifyItem {
148 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
149
150 NSString* account = @"account-delete-me";
151
152 [self startCKKSSubsystem];
153
154 // We expect a single record to be uploaded.
155 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
156 [self addGenericPassword: @"data" account: account];
157 OCMVerifyAllWithDelay(self.mockDatabase, 20);
158
159 // And then modified.
160 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
161 [self updateGenericPassword: @"otherdata" account:account];
162 OCMVerifyAllWithDelay(self.mockDatabase, 20);
163 }
164
165 - (void)testModifyItemImmediately {
166 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
167 NSString* account = @"account-delete-me";
168
169 [self startCKKSSubsystem];
170 [self holdCloudKitModifications];
171
172 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
173 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
174 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
175 [self addGenericPassword: @"data" account: account];
176 OCMVerifyAllWithDelay(self.mockDatabase, 20);
177
178 // Right now, the write in CloudKit is pending. Make the local modification...
179 [self updateGenericPassword: @"otherdata" account:account];
180
181 // And then schedule the update
182 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
183 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
184 [self releaseCloudKitModificationHold];
185
186 OCMVerifyAllWithDelay(self.mockDatabase, 20);
187 }
188
189 - (void)testModifyItemPrimaryKey {
190 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
191
192 NSString* account = @"account-delete-me";
193
194 [self startCKKSSubsystem];
195
196 // We expect a single record to be uploaded.
197 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
198 [self addGenericPassword: @"data" account: account];
199 OCMVerifyAllWithDelay(self.mockDatabase, 20);
200
201 // And then modified. Since we're changing the "primary key", we expect to delete the old record and upload a new one.
202 [self expectCKModifyItemRecords:1 deletedRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:nil];
203 [self updateAccountOfGenericPassword: @"new-account-delete-me" account:account];
204 OCMVerifyAllWithDelay(self.mockDatabase, 20);
205 }
206
207 - (void)testModifyItemDuringReencrypt {
208 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
209 NSString* account = @"account-delete-me";
210
211 [self startCKKSSubsystem];
212 [self holdCloudKitModifications];
213
214 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
215 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
216 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
217 [self addGenericPassword: @"data" account: account];
218 OCMVerifyAllWithDelay(self.mockDatabase, 20);
219
220 // Right now, the write in CloudKit is pending. Make the local modification...
221 [self updateGenericPassword: @"otherdata" account:account];
222
223 // And then schedule the update
224 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
225 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
226
227 // Stop the reencrypt operation from happening
228 self.keychainView.holdReencryptOutgoingItemsOperation = [CKKSGroupOperation named:@"reencrypt-hold" withBlock: ^{
229 secnotice("ckks", "releasing reencryption hold");
230 }];
231
232 // The cloudkit operation finishes, letting the next OQO proceed (and set up the reencryption operation)
233 [self releaseCloudKitModificationHold];
234
235 // And wait for this to finish...
236 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
237 // And once more to quiesce.
238 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
239
240 // Pause outgoing queue operations to ensure the reencryption operation runs first
241 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
242 secnotice("ckks", "releasing outgoing-queue hold");
243 }];
244
245 // Run the reencrypt items operation to completion.
246 [self.operationQueue addOperation: self.keychainView.holdReencryptOutgoingItemsOperation];
247 [self.keychainView waitForOperationsOfClass:[CKKSReencryptOutgoingItemsOperation class]];
248
249 [self.operationQueue addOperation: self.keychainView.holdOutgoingQueueOperation];
250
251 OCMVerifyAllWithDelay(self.mockDatabase, 20);
252 [self.keychainView waitUntilAllOperationsAreFinished];
253 [self waitForCKModifications];
254 }
255
256 - (void)testModifyItemBeforeReencrypt {
257 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
258 NSString* account = @"account-delete-me";
259
260 [self startCKKSSubsystem];
261 [self holdCloudKitModifications];
262
263 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
264 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
265 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
266 [self addGenericPassword: @"data" account: account];
267 OCMVerifyAllWithDelay(self.mockDatabase, 20);
268
269 // Right now, the write in CloudKit is pending. Make the local modification...
270 [self updateGenericPassword: @"otherdata" account:account];
271
272 // And then schedule the update, but for the final version of the password
273 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
274 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"third"]];
275
276 // Stop the reencrypt operation from happening
277 self.keychainView.holdReencryptOutgoingItemsOperation = [CKKSGroupOperation named:@"reencrypt-hold" withBlock: ^{
278 secnotice("ckks", "releasing reencryption hold");
279 }];
280
281 // The cloudkit operation finishes, letting the next OQO proceed (and set up the reencryption operation)
282 [self releaseCloudKitModificationHold];
283
284 // And wait for this to finish...
285 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
286 // And once more to quiesce.
287 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
288
289 [self updateGenericPassword: @"third" account:account];
290
291 // Item should upload.
292 OCMVerifyAllWithDelay(self.mockDatabase, 20);
293
294 // Run the reencrypt items operation to completion.
295 [self.operationQueue addOperation: self.keychainView.holdReencryptOutgoingItemsOperation];
296 [self.keychainView waitForOperationsOfClass:[CKKSReencryptOutgoingItemsOperation class]];
297
298 [self.keychainView waitUntilAllOperationsAreFinished];
299 [self waitForCKModifications];
300 }
301
302 - (void)testModifyItemDuringNetworkFailure {
303 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
304 NSString* account = @"account-delete-me";
305
306 [self startCKKSSubsystem];
307 [self holdCloudKitModifications];
308
309 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
310 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
311
312 [self addGenericPassword: @"data" account: account];
313 OCMVerifyAllWithDelay(self.mockDatabase, 20);
314
315 // Right now, the write in CloudKit is pending. Make the local modification...
316 [self updateGenericPassword: @"otherdata" account:account];
317
318 // And then schedule the update, but for the final version of the password
319 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
320 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
321
322 // The cloudkit operation finishes, letting the next OQO proceed (and set up uploading the new item)
323 [self releaseCloudKitModificationHold];
324
325 // Item should upload.
326 OCMVerifyAllWithDelay(self.mockDatabase, 20);
327
328 [self.keychainView waitUntilAllOperationsAreFinished];
329 [self waitForCKModifications];
330 }
331
332 - (void)testOutgoingQueueRecoverFromStaleInflightEntry {
333 // CKKS is restarting with an existing in-flight OQE
334 // Note that this test is incomplete, and doesn't re-add the item to the local keychain
335 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
336 NSString* account = @"fake-account";
337
338 [self.keychainView dispatchSync:^bool {
339 NSError* error = nil;
340
341 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
342
343 CKKSItem* item = [self newItem:ckrid withNewItemData:[self fakeRecordDictionary:account zoneID:self.keychainZoneID] key:self.keychainZoneKeys.classC];
344 XCTAssertNotNil(item, "Should be able to create a new fake item");
345
346 CKKSOutgoingQueueEntry* oqe = [[CKKSOutgoingQueueEntry alloc] initWithCKKSItem:item action:SecCKKSActionAdd state:SecCKKSStateInFlight waitUntil:nil accessGroup:@"ckks"];
347 XCTAssertNotNil(oqe, "Should be able to create a new fake OQE");
348 [oqe saveToDatabase:&error];
349
350 XCTAssertNil(error, "Shouldn't error saving new OQE to database");
351 return true;
352 }];
353
354 NSError *error = NULL;
355 XCTAssertEqual([CKKSOutgoingQueueEntry countByState:SecCKKSStateInFlight zone:self.keychainZoneID error:&error], 1,
356 "Expected on inflight entry in outgoing queue: %@", error);
357
358 // When CKKS restarts, it should find and re-upload this item
359 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
360 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
361
362 [self startCKKSSubsystem];
363 [self.keychainView waitForFetchAndIncomingQueueProcessing];
364
365 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
366 [self.keychainView waitForKeyHierarchyReadiness];
367 OCMVerifyAllWithDelay(self.mockDatabase, 20);
368 }
369
370 - (void)testOutgoingQueueRecoverFromNetworkFailure {
371 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
372 NSString* account = @"account-delete-me";
373
374 [self startCKKSSubsystem];
375 [self holdCloudKitModifications];
376
377 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
378
379 NSError* greyMode = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNotAuthenticated userInfo:@{}];
380 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:greyMode];
381
382 [self addGenericPassword: @"data" account: account];
383 OCMVerifyAllWithDelay(self.mockDatabase, 20);
384
385 // And then schedule the retried update
386 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
387 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
388
389 // The cloudkit operation finishes, letting the next OQO proceed (and set up uploading the new item)
390 [self releaseCloudKitModificationHold];
391
392 OCMVerifyAllWithDelay(self.mockDatabase, 20);
393
394 [self.keychainView waitUntilAllOperationsAreFinished];
395 [self waitForCKModifications];
396 }
397
398 - (void)testDeleteItem {
399 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
400
401 [self startCKKSSubsystem];
402
403 // We expect a single record to be uploaded.
404 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
405 [self addGenericPassword: @"data" account: @"account-delete-me"];
406 OCMVerifyAllWithDelay(self.mockDatabase, 20);
407
408 // We expect a single record to be deleted.
409 [self expectCKDeleteItemRecords: 1 zoneID:self.keychainZoneID];
410 [self deleteGenericPassword:@"account-delete-me"];
411 OCMVerifyAllWithDelay(self.mockDatabase, 20);
412 }
413
414 - (void)testDeleteItemImmediatelyAfterModify {
415 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
416 NSString* account = @"account-delete-me";
417
418 [self startCKKSSubsystem];
419
420 // We expect a single record to be uploaded.
421 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
422 [self addGenericPassword: @"data" account: account];
423 OCMVerifyAllWithDelay(self.mockDatabase, 20);
424
425 // Now, hold the modify
426 [self holdCloudKitModifications];
427
428 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
429 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
430 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
431
432 [self updateGenericPassword: @"otherdata" account:account];
433 OCMVerifyAllWithDelay(self.mockDatabase, 20);
434
435 // Right now, the write in CloudKit is pending. Make the local deletion...
436 [self deleteGenericPassword:account];
437
438 // And then schedule the update
439 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
440 [self releaseCloudKitModificationHold];
441
442 OCMVerifyAllWithDelay(self.mockDatabase, 20);
443 }
444
445 - (void)testDeleteItemAfterFetchAfterModify {
446 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
447 NSString* account = @"account-delete-me";
448
449 [self startCKKSSubsystem];
450
451 // We expect a single record to be uploaded.
452 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
453 [self addGenericPassword: @"data" account: account];
454 OCMVerifyAllWithDelay(self.mockDatabase, 20);
455
456 // Now, hold the modify
457 //[self holdCloudKitModifications];
458
459 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
460 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
461 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
462
463 [self updateGenericPassword: @"otherdata" account:account];
464 OCMVerifyAllWithDelay(self.mockDatabase, 20);
465
466 // Right now, the write in CloudKit is pending. Place a hold on outgoing queue processing
467 // Place a hold on processing the outgoing queue.
468 CKKSResultOperation* blockOutgoing = [CKKSResultOperation operationWithBlock:^{
469 secnotice("ckks", "Outgoing queue hold released.");
470 }];
471 blockOutgoing.name = @"outgoing-queue-hold";
472 CKKSResultOperation* outgoingQueueOperation = [self.keychainView processOutgoingQueueAfter:blockOutgoing ckoperationGroup:nil];
473
474 [self deleteGenericPassword:account];
475
476 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
477
478 // Release the CK modification hold
479 //[self releaseCloudKitModificationHold];
480
481 // And cause a fetch
482 [self.keychainView waitForFetchAndIncomingQueueProcessing];
483 [self.operationQueue addOperation:blockOutgoing];
484 [outgoingQueueOperation waitUntilFinished];
485
486 OCMVerifyAllWithDelay(self.mockDatabase, 20);
487 }
488
489
490 - (void)testReceiveItem {
491 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
492 [self startCKKSSubsystem];
493
494 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
495 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
496 (id)kSecAttrAccount : @"account-delete-me",
497 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
498 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
499 };
500
501 CFTypeRef item = NULL;
502 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
503
504 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
505 [self.keychainZone addToZone: ckr];
506
507 // Trigger a notification (with hilariously fake data)
508 [self.keychainView notifyZoneChange:nil];
509
510 [self.keychainView waitForFetchAndIncomingQueueProcessing];
511 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
512 }
513
514 - (void)testReceiveManyItems {
515 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
516 [self startCKKSSubsystem];
517
518 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D00" withAccount:@"account0"]];
519 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D01" withAccount:@"account1"]];
520 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D02" withAccount:@"account2"]];
521 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D03" withAccount:@"account3"]];
522 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D04" withAccount:@"account4"]];
523 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D05" withAccount:@"account5"]];
524 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D06" withAccount:@"account6"]];
525 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D07" withAccount:@"account7"]];
526 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D08" withAccount:@"account8"]];
527 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D09" withAccount:@"account9"]];
528 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D10" withAccount:@"account10"]];
529 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D11" withAccount:@"account11"]];
530
531 for(int i = 12; i < 100; i++) {
532 @autoreleasepool {
533 NSString* recordName = [NSString stringWithFormat:@"7B598D31-F9C5-481E-98AC-%012d", i];
534 NSString* account = [NSString stringWithFormat:@"account%d", i];
535
536 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:recordName withAccount:account]];
537 }
538 }
539
540 // Trigger a notification (with hilariously fake data)
541 [self.keychainView notifyZoneChange:nil];
542
543 [self.keychainView waitForFetchAndIncomingQueueProcessing];
544
545 [self findGenericPassword: @"account0" expecting:errSecSuccess];
546 [self findGenericPassword: @"account1" expecting:errSecSuccess];
547 [self findGenericPassword: @"account2" expecting:errSecSuccess];
548 [self findGenericPassword: @"account3" expecting:errSecSuccess];
549 [self findGenericPassword: @"account4" expecting:errSecSuccess];
550 [self findGenericPassword: @"account5" expecting:errSecSuccess];
551 [self findGenericPassword: @"account6" expecting:errSecSuccess];
552 [self findGenericPassword: @"account7" expecting:errSecSuccess];
553 [self findGenericPassword: @"account8" expecting:errSecSuccess];
554 [self findGenericPassword: @"account9" expecting:errSecSuccess];
555 [self findGenericPassword: @"account10" expecting:errSecSuccess];
556 [self findGenericPassword: @"account11" expecting:errSecSuccess];
557 }
558
559 - (void)testReceiveCollidingItem {
560 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
561 [self startCKKSSubsystem];
562
563 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
564 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
565 (id)kSecAttrAccount : @"account-delete-me",
566 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
567 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
568 };
569
570 CFTypeRef item = NULL;
571 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
572
573 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
574 CKRecord* ckr2 = [self createFakeRecord: self.keychainZoneID recordName: @"F9C58D31-7B59-481E-98AC-5A507ACB2D85"];
575
576 [self.keychainZone addToZone: ckr];
577 [self.keychainZone addToZone: ckr2];
578
579 // We expect a delete operation with the "higher" UUID.
580 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
581
582 // Trigger a notification (with hilariously fake data)
583 [self.keychainView notifyZoneChange:nil];
584
585 OCMVerifyAllWithDelay(self.mockDatabase, 20);
586 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
587
588 [self waitForCKModifications];
589 XCTAssertNil(self.keychainZone.currentDatabase[ckr2.recordID], "Correct record was deleted from CloudKit");
590 }
591
592 -(void)testReceiveItemDelete {
593 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
594 [self startCKKSSubsystem];
595
596 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
597 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
598 (id)kSecAttrAccount : @"account-delete-me",
599 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
600 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
601 };
602
603 CFTypeRef item = NULL;
604 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
605
606 [self.keychainView waitForFetchAndIncomingQueueProcessing];
607
608 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
609 [self.keychainZone addToZone: ckr];
610
611 // Trigger a notification (with hilariously fake data)
612 [self.keychainView notifyZoneChange:nil];
613 [self.keychainView waitForFetchAndIncomingQueueProcessing];
614
615 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
616 CFReleaseNull(item);
617
618 // Trigger a delete
619 [self.keychainZone deleteCKRecordIDFromZone: [ckr recordID]];
620 [self.keychainView notifyZoneChange:nil];
621 [self.keychainView waitForFetchAndIncomingQueueProcessing];
622
623 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
624 }
625
626 -(void)testReceiveItemPhantomDelete {
627 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
628 [self startCKKSSubsystem];
629
630 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
631 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
632 (id)kSecAttrAccount : @"account-delete-me",
633 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
634 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
635 };
636
637 CFTypeRef item = NULL;
638 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
639
640 [self.keychainView waitForFetchAndIncomingQueueProcessing];
641
642 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
643 [self.keychainZone addToZone: ckr];
644
645 // Trigger a notification (with hilariously fake data)
646 [self.keychainView notifyZoneChange:nil];
647 [self.keychainView waitForFetchAndIncomingQueueProcessing];
648
649 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
650 CFReleaseNull(item);
651
652 [self.keychainView waitUntilAllOperationsAreFinished];
653
654 // Trigger a delete
655 [self.keychainZone deleteCKRecordIDFromZone: [ckr recordID]];
656
657 // and add another, incorrect IQE
658 [self.keychainView dispatchSync: ^bool {
659 // Inefficient, but hey, it works
660 CKRecord* record = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-FFFF-FFFF-5A507ACB2D85"];
661 CKKSItem* fakeItem = [[CKKSItem alloc] initWithCKRecord: record];
662
663 CKKSIncomingQueueEntry* iqe = [[CKKSIncomingQueueEntry alloc] initWithCKKSItem:fakeItem
664 action:SecCKKSActionDelete
665 state:SecCKKSStateNew];
666 XCTAssertNotNil(iqe, "could create fake IQE");
667 NSError* error = nil;
668 XCTAssert([iqe saveToDatabase: &error], "Saved fake IQE to database");
669 XCTAssertNil(error, "No error saving fake IQE to database");
670 return true;
671 }];
672
673 [self.keychainView notifyZoneChange:nil];
674 [self.keychainView waitForFetchAndIncomingQueueProcessing];
675
676 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
677
678 // The incoming queue should be empty
679 [self.keychainView dispatchSync: ^bool {
680 NSError* error = nil;
681 NSArray* iqes = [CKKSIncomingQueueEntry all:&error];
682 XCTAssertNil(error, "No error loading IQEs");
683 XCTAssertNotNil(iqes, "Could load IQEs");
684 XCTAssertEqual(iqes.count, 0u, "Incoming queue is empty");
685 }];
686 }
687
688 -(void)testReceiveConflictOnJustAddedItem {
689 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
690 [self startCKKSSubsystem];
691
692 [self.keychainView waitForKeyHierarchyReadiness];
693 [self.keychainView waitUntilAllOperationsAreFinished];
694
695 // Place a hold on processing the outgoing queue.
696 CKKSResultOperation* blockOutgoing = [CKKSResultOperation operationWithBlock:^{
697 secnotice("ckks", "Outgoing queue hold released.");
698 }];
699 blockOutgoing.name = @"outgoing-queue-hold";
700 CKKSResultOperation* outgoingQueueOperation = [self.keychainView processOutgoingQueueAfter:blockOutgoing ckoperationGroup:nil];
701
702 CKKSResultOperation* blockIncoming = [CKKSResultOperation operationWithBlock:^{
703 secnotice("ckks", "Incoming queue hold released.");
704 }];
705 blockIncoming.name = @"incoming-queue-hold";
706 CKKSResultOperation* incomingQueueOperation = [self.keychainView processIncomingQueue:false after: blockIncoming];
707
708 [self addGenericPassword:@"localchange" account:@"account-delete-me"];
709
710 // Pull out the new item's UUID.
711 __block NSString* itemUUID = nil;
712 [self.keychainView dispatchSync:^bool {
713 NSError* error = nil;
714 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID ?: [[CKRecordZoneID alloc] initWithZoneName:@"keychain"
715 ownerName:CKCurrentUserDefaultName]
716 error:&error];
717 XCTAssertNil(error, "no error fetching uuids");
718 XCTAssertEqual(uuids.count, 1u, "There's exactly one outgoing queue entry");
719 itemUUID = uuids[0];
720
721 XCTAssertNotNil(itemUUID, "Have a UUID for our new item");
722 return false;
723 }];
724
725 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName: itemUUID]];
726
727 [self.keychainView notifyZoneChange:nil];
728 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
729
730 // Allow the outgoing queue operation to proceed
731 [self.operationQueue addOperation:blockOutgoing];
732 [outgoingQueueOperation waitUntilFinished];
733
734 // Allow the incoming queue operation to proceed
735 [self.operationQueue addOperation:blockIncoming];
736 [incomingQueueOperation waitUntilFinished];
737
738 [self checkGenericPassword:@"data" account:@"account-delete-me"];
739
740 [self.keychainView waitUntilAllOperationsAreFinished];
741 }
742
743 - (void)testReceiveCloudKitConflictOnJustAddedItems {
744 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
745 [self startCKKSSubsystem];
746
747 [self.keychainView waitForKeyHierarchyReadiness];
748 [self.keychainView waitUntilAllOperationsAreFinished];
749
750 // Place a hold on processing the outgoing queue.
751 self.keychainView.holdOutgoingQueueOperation = [CKKSResultOperation named:@"outgoing-queue-hold" withBlock:^{
752 secnotice("ckks", "Outgoing queue hold released.");
753 }];
754
755 [self addGenericPassword:@"localchange" account:@"account-delete-me"];
756
757 // Pull out the new item's UUID.
758 __block NSString* itemUUID = nil;
759 [self.keychainView dispatchSync:^bool {
760 NSError* error = nil;
761 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID ?: [[CKRecordZoneID alloc] initWithZoneName:@"keychain"
762 ownerName:CKCurrentUserDefaultName]
763 error:&error];
764 XCTAssertNil(error, "no error fetching uuids");
765 XCTAssertEqual(uuids.count, 1u, "There's exactly one outgoing queue entry");
766 itemUUID = uuids[0];
767
768 XCTAssertNotNil(itemUUID, "Have a UUID for our new item");
769 return false;
770 }];
771
772 // Add a second item: this item should be uploaded after the failure of the first item
773 [self addGenericPassword:@"localchange" account:@"account-delete-me-2"];
774
775 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName: itemUUID]];
776
777 // Also, this write will increment the class C current pointer's etag
778 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
779 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
780 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
781 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
782 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
783
784 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
785 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
786 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
787
788 // Allow the outgoing queue operation to proceed
789 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
790
791 OCMVerifyAllWithDelay(self.mockDatabase, 20);
792 [self.keychainView waitUntilAllOperationsAreFinished];
793
794 [self checkGenericPassword:@"data" account:@"account-delete-me"];
795 [self checkGenericPassword:@"localchange" account:@"account-delete-me-2"];
796 }
797
798
799 -(void)testReceiveUnknownField {
800 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
801
802 [self startCKKSSubsystem];
803 [self.keychainView waitForKeyHierarchyReadiness];
804
805 NSError* error = nil;
806
807 // Manually encrypt an item
808 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
809 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
810 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
811 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
812 parentKeyUUID:self.keychainZoneKeys.classA.uuid
813 zoneID:recordID.zoneID];
814 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classA error:&error];
815 XCTAssertNotNil(itemkey, "Got a key");
816 cipheritem.wrappedkey = itemkey.wrappedkey;
817 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
818
819 NSData* future_data_field = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding];
820 NSString* future_string_field = @"authstring";
821 NSString* future_server_field = @"server_can_change_at_any_time";
822 NSNumber* future_number_field = [NSNumber numberWithInt:30];
823
824 // Use version 2, so future fields will be authenticated
825 cipheritem.encver = CKKSItemEncryptionVersion2;
826 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
827
828 authenticatedData[@"future_data_field"] = future_data_field;
829 authenticatedData[@"future_string_field"] = [future_string_field dataUsingEncoding:NSUTF8StringEncoding];
830
831 uint64_t n = OSSwapHostToLittleConstInt64([future_number_field unsignedLongValue]);
832 authenticatedData[@"future_number_field"] = [NSData dataWithBytes:&n length:sizeof(n)];
833
834
835 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
836 XCTAssertNil(error, "no error encrypting object");
837 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
838
839 CKRecord* ckr = [cipheritem CKRecordWithZoneID: recordID.zoneID];
840 ckr[@"future_data_field"] = future_data_field;
841 ckr[@"future_string_field"] = future_string_field;
842 ckr[@"future_number_field"] = future_number_field;
843 ckr[@"server_new_server_field"] = future_server_field;
844 [self.keychainZone addToZone:ckr];
845
846 [self.keychainView waitForFetchAndIncomingQueueProcessing];
847
848 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
849 (id)kSecReturnAttributes: @YES,
850 (id)kSecAttrSynchronizable: @YES,
851 (id)kSecAttrAccount: @"account-delete-me",
852 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
853 };
854 CFTypeRef cfresult = NULL;
855 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
856
857 // Test that if this item is updated, it remains encrypted in v2, and future_field still exists
858 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
859 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
860
861 OCMVerifyAllWithDelay(self.mockDatabase, 20);
862 [self waitForCKModifications];
863
864 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
865 XCTAssertEqualObjects(newRecord[@"future_data_field"], future_data_field, "future_data_field still exists");
866 XCTAssertEqualObjects(newRecord[@"future_string_field"], future_string_field, "future_string_field still exists");
867 XCTAssertEqualObjects(newRecord[@"future_number_field"], future_number_field, "future_string_field still exists");
868 XCTAssertEqualObjects(newRecord[@"server_new_server_field"], future_server_field, "future_server_field stille exists");
869
870 CKKSItem* newItem = [[CKKSItem alloc] initWithCKRecord:newRecord];
871 CKKSAESSIVKey* newItemKey = [self.keychainZoneKeys.classA unwrapAESKey:newItem.wrappedkey error:&error];
872 XCTAssertNil(error, "No error unwrapping AES key");
873 XCTAssertNotNil(newItemKey, "Have an unwrapped AES key for this item");
874
875 NSDictionary* uploadedData = [CKKSItemEncrypter decryptDictionary:newRecord[SecCKRecordDataKey]
876 key:newItemKey
877 authenticatedData:authenticatedData
878 error:&error];
879 XCTAssertNil(error, "No error decrypting dictionary");
880 XCTAssertNotNil(uploadedData, "Authenticated re-uploaded data including future_field");
881 XCTAssertEqualObjects(uploadedData[@"v_Data"], [@"different password" dataUsingEncoding:NSUTF8StringEncoding], "Passwords match");
882 }
883
884
885 -(void)testReceiveRecordEncryptedv1 {
886 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
887
888 [self startCKKSSubsystem];
889 [self.keychainView waitForKeyHierarchyReadiness];
890
891 NSError* error = nil;
892
893 // Manually encrypt an item
894 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
895 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
896 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
897 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
898 parentKeyUUID:self.keychainZoneKeys.classC.uuid
899 zoneID:recordID.zoneID];
900 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
901 XCTAssertNotNil(itemkey, "Got a key");
902 cipheritem.wrappedkey = itemkey.wrappedkey;
903 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
904
905 cipheritem.encver = CKKSItemEncryptionVersion1;
906
907 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:cipheritem.encver] mutableCopy];
908
909 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
910 XCTAssertNil(error, "no error encrypting object");
911 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
912
913 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
914
915 [self.keychainView waitForFetchAndIncomingQueueProcessing];
916
917 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
918 (id)kSecReturnAttributes: @YES,
919 (id)kSecAttrSynchronizable: @YES,
920 (id)kSecAttrAccount: @"account-delete-me",
921 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
922 };
923 CFTypeRef cfresult = NULL;
924 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
925 CFReleaseNull(cfresult);
926
927 // Test that if this item is updated, it is encrypted in v2
928 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
929 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
930
931 OCMVerifyAllWithDelay(self.mockDatabase, 20);
932 [self waitForCKModifications];
933
934 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
935 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
936 }
937
938 - (void)testUploadPagination {
939 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
940
941 for(size_t count = 0; count < 250; count++) {
942 [self addGenericPassword: @"data" account: [NSString stringWithFormat:@"account-delete-me-%03lu", count]];
943 }
944
945 [self startCKKSSubsystem];
946
947 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
948 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
949 [self expectCKModifyItemRecords: 50 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
950
951 OCMVerifyAllWithDelay(self.mockDatabase, 20);
952 }
953
954 - (void)testUploadInitialKeyHierarchy {
955 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
956 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
957
958 // Spin up CKKS subsystem.
959 [self startCKKSSubsystem];
960
961 OCMVerifyAllWithDelay(self.mockDatabase, 20);
962 }
963
964 - (void)testUploadInitialKeyHierarchyAfterLockedStart {
965 // 'Lock' the keybag
966 self.aksLockState = true;
967 [self.lockStateTracker recheck];
968
969 [self startCKKSSubsystem];
970
971 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
972 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitforunlock");
973
974 // After unlock, the key hierarchy should be created.
975 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
976
977 self.aksLockState = false;
978 [self.lockStateTracker recheck];
979
980 OCMVerifyAllWithDelay(self.mockDatabase, 20);
981
982 // We expect a single class C record to be uploaded.
983 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
984
985 [self addGenericPassword: @"data" account: @"account-delete-me"];
986 OCMVerifyAllWithDelay(self.mockDatabase, 20);
987 }
988
989 - (void)testLockImmediatelyAfterUploadingInitialKeyHierarchy {
990
991 // Upon upload, block fetches
992 __weak __typeof(self) weakSelf = self;
993 [self expectCKModifyRecords: @{
994 SecCKRecordIntermediateKeyType: [NSNumber numberWithUnsignedInteger: 3],
995 SecCKRecordCurrentKeyType: [NSNumber numberWithUnsignedInteger: 3],
996 SecCKRecordTLKShareType: [NSNumber numberWithUnsignedInteger: 1],
997 }
998 deletedRecordTypeCounts:nil
999 zoneID:self.keychainZoneID
1000 checkModifiedRecord:nil
1001 runAfterModification:^{
1002 __strong __typeof(self) strongSelf = weakSelf;
1003 [strongSelf holdCloudKitFetches];
1004 }];
1005
1006 [self startCKKSSubsystem];
1007
1008 // Should enter 'ready'
1009 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1010 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1011
1012 // Now, lock and allow fetches again
1013 self.aksLockState = true;
1014 [self.lockStateTracker recheck];
1015 [self releaseCloudKitFetchHold];
1016
1017 CKKSResultOperation* op = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
1018 [op waitUntilFinished];
1019
1020 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1021
1022 // Wait for CKKS to shake itself out...
1023 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
1024
1025 // Should be in ReadyPendingUnlock
1026 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1027
1028 // We expect a single class C record to be uploaded.
1029 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1030 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1031
1032 [self addGenericPassword: @"data" account: @"account-delete-me"];
1033 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1034 }
1035
1036 - (void)testReceiveKeyHierarchyAfterLockedStart {
1037 // 'Lock' the keybag
1038 self.aksLockState = true;
1039 [self.lockStateTracker recheck];
1040
1041 [self startCKKSSubsystem];
1042
1043 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1044 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateFetchComplete] wait:20*NSEC_PER_SEC], @"Key state should get stuck in fetchcomplete");
1045
1046 // Now, another device comes along and creates the hierarchy; we download it; and it and sends us the TLK
1047 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1048 [self.keychainView notifyZoneChange:nil];
1049 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
1050
1051 self.aksLockState = false;
1052 [self.lockStateTracker recheck];
1053
1054 // After unlock, the TLK arrives
1055 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1056 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1057
1058 // We expect a single class C record to be uploaded.
1059 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1060
1061 [self addGenericPassword: @"data" account: @"account-delete-me"];
1062 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1063 }
1064
1065 - (void)testLoadKeyHierarchyAfterLockedStart {
1066 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
1067
1068 // 'Lock' the keybag
1069 self.aksLockState = true;
1070 [self.lockStateTracker recheck];
1071
1072 [self startCKKSSubsystem];
1073
1074 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1075 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1076
1077 self.aksLockState = false;
1078 [self.lockStateTracker recheck];
1079
1080 // We expect a single class C record to be uploaded.
1081 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1082
1083 [self addGenericPassword: @"data" account: @"account-delete-me"];
1084 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1085 }
1086
1087 - (void)testUploadAndUseKeyHierarchy {
1088 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
1089 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1090
1091 [self startCKKSSubsystem];
1092
1093 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
1094 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1095 (id)kSecAttrAccount : @"account-delete-me",
1096 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1097 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1098 };
1099 CFTypeRef item = NULL;
1100 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not exist");
1101
1102 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1103 [self waitForCKModifications];
1104
1105 // We expect a single class C record to be uploaded.
1106 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1107
1108 [self addGenericPassword: @"data" account: @"account-delete-me"];
1109 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1110
1111 // now, expect a single class A record to be uploaded
1112 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1113
1114 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef)@{
1115 (id)kSecClass : (id)kSecClassGenericPassword,
1116 (id)kSecAttrAccessGroup : @"com.apple.security.sos",
1117 (id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked,
1118 (id)kSecAttrAccount : @"account-class-A",
1119 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1120 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
1121 }, NULL), @"Adding class A item");
1122 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1123 }
1124
1125 - (void)testUploadInitialKeyHierarchyTriggersBackup {
1126 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
1127 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1128
1129 // We also expect the view manager's notifyNewTLKsInKeychain call to fire (after some delay)
1130 OCMExpect([self.mockCKKSViewManager notifyNewTLKsInKeychain]);
1131
1132 // Spin up CKKS subsystem.
1133 [self startCKKSSubsystem];
1134
1135 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1136 OCMVerifyAllWithDelay(self.mockCKKSViewManager, 10);
1137 }
1138
1139 - (void)testResetCloudKitZoneFromNoTLK {
1140 self.silentZoneDeletesAllowed = true;
1141
1142 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1143 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1144 // explicitly do not save a fake device status here
1145 self.keychainZone.flag = true;
1146
1147 // It'll eventually upload a new key hierarchy
1148 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
1149
1150 [self startCKKSSubsystem];
1151 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1152
1153 // But then, it'll fire off the reset and reach 'ready'
1154 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1155 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1156
1157 // And the zone should have been cleared and re-made
1158 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1159 }
1160
1161 - (void)testResetCloudKitZoneFromNoTLKWithOtherWaitForTLKDevices {
1162 self.silentZoneDeletesAllowed = true;
1163
1164 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1165 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1166 // Save a fake device status here, but modify its key state to be 'waitfortlk': it has no idea what the TLK is either
1167 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1168
1169 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1170 if([record.recordType isEqualToString:SecCKRecordDeviceStateType]) {
1171 record[SecCKRecordKeyState] = CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTLK);
1172 }
1173 }
1174
1175 self.keychainZone.flag = true;
1176
1177 // It'll eventually upload a new key hierarchy
1178 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
1179
1180 [self startCKKSSubsystem];
1181 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1182
1183 // But then, it'll fire off the reset and reach 'ready'
1184 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1185 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1186
1187 // And the zone should have been cleared and re-made
1188 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1189 }
1190
1191 - (void)testResetCloudKitZoneFromNoTLKIgnoringInactiveDevices {
1192 self.silentZoneDeletesAllowed = true;
1193
1194 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1195 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1196 // Save a fake device status here, but modify its creation and modification times to be months ago
1197 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1198
1199 // Put a 'in-circle' TLKShare record, but also modify its creation and modification times
1200 CKKSSOSSelfPeer* untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"untrusted-peer"
1201 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1202 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
1203 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:untrustedPeer to:untrustedPeer zoneID:self.keychainZoneID];
1204
1205 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1206 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1207 record.creationDate = [NSDate distantPast];
1208 record.modificationDate = [NSDate distantPast];
1209 }
1210 }
1211
1212 self.keychainZone.flag = true;
1213
1214 // It'll eventually upload a new key hierarchy
1215 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
1216
1217 [self startCKKSSubsystem];
1218 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1219
1220 // But then, it'll fire off the reset and reach 'ready'
1221 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1222 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1223
1224 // And the zone should have been cleared and re-made
1225 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1226 }
1227
1228 - (void)testDoNotResetCloudKitZoneFromWaitForTLKDueToRecentDeviceState {
1229 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1230
1231 // CKKS shouldn't reset this zone, due to a recent device status claiming to have TLKs
1232 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1233
1234 NSDateComponents* offset = [[NSDateComponents alloc] init];
1235 [offset setDay:-5];
1236 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1237 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1238 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1239 record.creationDate = updateTime;
1240 record.modificationDate = updateTime;
1241 }
1242 }
1243
1244 self.keychainZone.flag = true;
1245 [self startCKKSSubsystem];
1246
1247 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1248
1249 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1250 }
1251
1252 - (void)testDoNotCloudKitZoneFromWaitForTLKDueToRecentButUntrustedDeviceState {
1253 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1254
1255 // CKKS should reset this zone, even though to a recent device status claiming to have TLKs. The device isn't trusted
1256 self.silentZoneDeletesAllowed = true;
1257 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1258 [self.currentPeers removeObject:self.remoteSOSOnlyPeer];
1259
1260 self.keychainZone.flag = true;
1261 [self startCKKSSubsystem];
1262
1263 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1264 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1265
1266 // And ensure it doesn't go on to 'reset'
1267 XCTAssertNotEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:100*NSEC_PER_MSEC], @"Key state should not become 'resetzone'");
1268 }
1269
1270 - (void)testResetCloudKitZoneFromWaitForTLKDueToLessRecentAndUntrustedDeviceState {
1271 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1272
1273 // CKKS should reset this zone, even though to a recent device status claiming to have TLKs. The device isn't trusted
1274 self.silentZoneDeletesAllowed = true;
1275 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1276 [self.currentPeers removeObject:self.remoteSOSOnlyPeer];
1277
1278 NSDateComponents* offset = [[NSDateComponents alloc] init];
1279 [offset setDay:-5];
1280 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1281 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1282 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1283 record.creationDate = updateTime;
1284 record.modificationDate = updateTime;
1285 }
1286 }
1287
1288 self.keychainZone.flag = true;
1289 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
1290 [self startCKKSSubsystem];
1291 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1292
1293 // Then we should reset.
1294 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1295 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1296
1297 // And the zone should have been cleared and re-made
1298 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1299 }
1300
1301 - (void)testDoNotResetCloudKitZoneFromWaitForTLKDueToAncientOctagonDeviceState {
1302 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1303
1304 // CKKS should not reset this zone, because some Octagon device appeared on this account ever
1305 self.silentZoneDeletesAllowed = true;
1306 [self putFakeOctagonOnlyDeviceStatusInCloudKit:self.keychainZoneID];
1307
1308 NSDateComponents* offset = [[NSDateComponents alloc] init];
1309 [offset setDay:-46];
1310 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1311 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1312 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1313 record.creationDate = updateTime;
1314 record.modificationDate = updateTime;
1315 }
1316 }
1317
1318 self.keychainZone.flag = true;
1319 [self startCKKSSubsystem];
1320
1321 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1322 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1323
1324 // And ensure it doesn't go on to 'reset'
1325 XCTAssertNotEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:100*NSEC_PER_MSEC], @"Key state should not become 'resetzone'");
1326 }
1327
1328 - (void)testAcceptExistingKeyHierarchy {
1329 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
1330 // Test also begins with the TLK having arrived in the local keychain (via SOS)
1331 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1332 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1333 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1334
1335 // Spin up CKKS subsystem.
1336 [self startCKKSSubsystem];
1337
1338 // The CKKS subsystem should only upload its TLK share
1339 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1340
1341 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1342
1343 // Verify that there are three local keys, and three local current key records
1344 __weak __typeof(self) weakSelf = self;
1345 [self.keychainView dispatchSync: ^bool{
1346 __strong __typeof(weakSelf) strongSelf = weakSelf;
1347 XCTAssertNotNil(strongSelf, "self exists");
1348
1349 NSError* error = nil;
1350
1351 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
1352 XCTAssertNil(error, "no error fetching keys");
1353 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
1354
1355 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1356 XCTAssertNil(error, "no error fetching current keys");
1357 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1358
1359 return false;
1360 }];
1361 }
1362
1363 - (void)testAcceptExistingAndUseKeyHierarchy {
1364 // Test starts with nothing in database, but one in our fake CloudKit.
1365 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1366 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1367 // But, CKKS shouldn't ever reset the zone
1368 self.keychainZone.flag = true;
1369
1370 [self startCKKSSubsystem];
1371 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should have become waitfortlk");
1372
1373 // Now, save the TLK to the keychain (to simulate it coming in later via SOS). We'll create a TLK share for ourselves.
1374 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1375 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1376
1377 // Wait for the key hierarchy to sort itself out, to make it easier on this test; see testOnboardOldItemsWithExistingKeyHierarchy for the other test.
1378 // The CKKS subsystem should write its TLK share, but nothing else
1379 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1380
1381 // We expect a single record to be uploaded for each key class
1382 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1383 [self addGenericPassword: @"data" account: @"account-delete-me"];
1384 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1385
1386 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1387 [self addGenericPassword:@"asdf"
1388 account:@"account-class-A"
1389 viewHint:nil
1390 access:(id)kSecAttrAccessibleWhenUnlocked
1391 expecting:errSecSuccess
1392 message:@"Adding class A item"];
1393 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1394 XCTAssertTrue(self.keychainZone.flag, "Keychain zone shouldn't have been reset");
1395 }
1396
1397 - (void)testAcceptExistingKeyHierarchyDespiteLocked {
1398 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
1399 // Test also begins with the TLK having arrived in the local keychain (via SOS)
1400
1401 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1402 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1403
1404 self.aksLockState = true;
1405 [self.lockStateTracker recheck];
1406
1407 // Spin up CKKS subsystem.
1408 [self startCKKSSubsystem];
1409
1410 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:20*NSEC_PER_SEC], "Key state should have become waitforunlock");
1411
1412 // CKKS will give itself a TLK Share
1413 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1414
1415 // Now that all operations are complete, 'unlock' AKS
1416 self.aksLockState = false;
1417 [self.lockStateTracker recheck];
1418
1419 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1420 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1421
1422 // Verify that there are three local keys, and three local current key records
1423 __weak __typeof(self) weakSelf = self;
1424 [self.keychainView dispatchSync: ^bool{
1425 __strong __typeof(weakSelf) strongSelf = weakSelf;
1426 XCTAssertNotNil(strongSelf, "self exists");
1427
1428 NSError* error = nil;
1429
1430 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
1431 XCTAssertNil(error, "no error fetching keys");
1432 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
1433
1434 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1435 XCTAssertNil(error, "no error fetching current keys");
1436 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1437
1438 return false;
1439 }];
1440 }
1441
1442 - (void)testReceiveClassCWhileALocked {
1443 // Test starts with a key hierarchy already existing.
1444 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
1445 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1446 [self startCKKSSubsystem];
1447
1448 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1449 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1450
1451 [self findGenericPassword:@"classCItem" expecting:errSecItemNotFound];
1452 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
1453
1454 // 'Lock' the keybag
1455 self.aksLockState = true;
1456 [self.lockStateTracker recheck];
1457
1458 XCTAssertNotNil(self.keychainZoneKeys, "Have zone keys for zone");
1459 XCTAssertNotNil(self.keychainZoneKeys.classA, "Have class A key for zone");
1460 XCTAssertNotNil(self.keychainZoneKeys.classC, "Have class C key for zone");
1461
1462 [self.keychainView dispatchSyncWithAccountKeys: ^bool {
1463 [self.keychainView _onqueueKeyStateMachineRequestProcess];
1464 return true;
1465 }];
1466 // And ensure we end up back in 'readypendingunlock': we have the keys, we're just locked now
1467 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
1468 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1469
1470 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"classCItem" key:self.keychainZoneKeys.classC]];
1471 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-FFFF-FFFF-FFFF-5A507ACB2D85" withAccount:@"classAItem" key:self.keychainZoneKeys.classA]];
1472
1473 CKKSResultOperation* op = [self.keychainView waitForFetchAndIncomingQueueProcessing];
1474 // The processing op should NOT error, even though it didn't manage to process the classA item
1475 XCTAssertNil(op.error, "no error while failing to process a class A item");
1476
1477 CKKSResultOperation* erroringOp = [self.keychainView processIncomingQueue:true];
1478 [erroringOp waitUntilFinished];
1479 XCTAssertNotNil(erroringOp.error, "error exists while processing a class A item");
1480
1481 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
1482 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
1483
1484 self.aksLockState = false;
1485 [self.lockStateTracker recheck];
1486 [self.keychainView waitUntilAllOperationsAreFinished];
1487
1488 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
1489 [self findGenericPassword:@"classAItem" expecting:errSecSuccess];
1490 }
1491
1492 - (void)testRestartWhileLocked {
1493 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1494 [self startCKKSSubsystem];
1495
1496 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1497
1498 // 'Lock' the keybag
1499 self.aksLockState = true;
1500 [self.lockStateTracker recheck];
1501
1502 [self.keychainView halt];
1503 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1504
1505 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1506
1507 self.aksLockState = false;
1508 [self.lockStateTracker recheck];
1509
1510 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1511 }
1512
1513 - (void)testExternalKeyRoll {
1514 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1515 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1516 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1517 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1518
1519 // Spin up CKKS subsystem.
1520 [self startCKKSSubsystem];
1521
1522 // The CKKS subsystem should not try to write anything to the CloudKit database.
1523 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1524
1525 __weak __typeof(self) weakSelf = self;
1526
1527 // We expect a single record to be uploaded.
1528 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1529
1530 [self addGenericPassword: @"data" account: @"account-delete-me"];
1531
1532 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1533 [self waitForCKModifications];
1534
1535 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1536 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1537 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1538
1539 // Trigger a notification
1540 [self.keychainView notifyZoneChange:nil];
1541
1542 // Make life easy on this test; testAcceptKeyConflictAndUploadReencryptedItem will check the case when we don't receive the notification
1543 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1544
1545 // Just in extra case of threading issues, force a reexamination of the key hierarchy
1546 [self.keychainView dispatchSyncWithAccountKeys: ^bool {
1547 [self.keychainView _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
1548 return true;
1549 }];
1550
1551 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1552
1553 // Verify that there are six local keys, and three local current key records
1554 [self.keychainView dispatchSync: ^bool{
1555 __strong __typeof(weakSelf) strongSelf = weakSelf;
1556 XCTAssertNotNil(strongSelf, "self exists");
1557
1558 NSError* error = nil;
1559 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:self.keychainZoneID error:&error];
1560 XCTAssertNil(error, "no error fetching keys");
1561 XCTAssertEqual(keys.count, 6u, "Six keys in local database");
1562
1563 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1564 XCTAssertNil(error, "no error fetching current keys");
1565 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1566
1567 for(CKKSCurrentKeyPointer* key in currentkeys) {
1568 if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
1569 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.tlk.uuid);
1570 } else if([key.keyclass isEqualToString: SecCKKSKeyClassA]) {
1571 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classA.uuid);
1572 } else if([key.keyclass isEqualToString: SecCKKSKeyClassC]) {
1573 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classC.uuid);
1574 } else {
1575 XCTFail("Unknown key class: %@", key.keyclass);
1576 }
1577 }
1578
1579 return false;
1580 }];
1581
1582 // We expect a single record to be uploaded.
1583 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1584
1585 // TODO: remove this by writing code for item reencrypt after key arrival
1586 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1587
1588 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1589
1590 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1591 }
1592
1593 - (void)testAcceptKeyConflictAndUploadReencryptedItem {
1594 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1595 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1596 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1597 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1598
1599 [self startCKKSSubsystem];
1600 [self.keychainView waitUntilAllOperationsAreFinished];
1601
1602 // We expect a single record to be uploaded.
1603 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1604
1605 [self addGenericPassword: @"data" account: @"account-delete-me"];
1606
1607 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1608 [self waitForCKModifications];
1609
1610 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1611
1612 // Do not trigger a notification here. This should cause a conflict updating the current key records
1613
1614 // We expect a single record to be uploaded, but that the write will be rejected
1615 // We then expect that item to be reuploaded with the current key
1616
1617 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
1618 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1619 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1620
1621 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under rolled class C key in hierarchy"]];
1622
1623 // New key arrives via SOS!
1624 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1625 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1626
1627 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1628 }
1629
1630 - (void)testAcceptKeyConflictAndUploadReencryptedItems {
1631 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1632 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1633 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1634 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1635
1636 [self startCKKSSubsystem];
1637 [self.keychainView waitUntilAllOperationsAreFinished];
1638
1639 // We expect a single record to be uploaded.
1640 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1641 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1642
1643 [self addGenericPassword: @"data" account: @"account-delete-me"];
1644
1645 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1646 [self waitForCKModifications];
1647
1648 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1649
1650 // Do not trigger a notification here. This should cause a conflict updating the current key records
1651
1652 // We expect a single record to be uploaded, but that the write will be rejected
1653 // We then expect that item to be reuploaded with the current key
1654
1655 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
1656 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1657 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key-2"];
1658 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1659
1660 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1661 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under rolled class C key in hierarchy"]];
1662
1663 // New key arrives via SOS!
1664 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1665 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1666
1667 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1668 }
1669
1670 - (void)testRecoverFromRequestKeyRefetchWithoutRolling {
1671 // Simply requesting a key state refetch shouldn't roll the key hierarchy.
1672
1673 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1674
1675 // Spin up CKKS subsystem.
1676 [self startCKKSSubsystem];
1677
1678 // Items should upload.
1679 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1680 [self addGenericPassword: @"data" account: @"account-delete-me"];
1681 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1682
1683 [self waitForCKModifications];
1684
1685
1686 // CKKS should not roll the keys while progressing back to 'ready', but it will fetch once
1687 self.silentFetchesAllowed = false;
1688 [self expectCKFetch];
1689
1690 [self.keychainView dispatchSyncWithAccountKeys: ^bool {
1691 [self.keychainView _onqueueKeyStateMachineRequestFetch];
1692 return true;
1693 }];
1694
1695 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
1696 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1697 }
1698
1699 - (void)testRecoverFromIncrementedCurrentKeyPointerEtag {
1700 // CloudKit sometimes reports the current key pointers have changed (etag mismatch), but their content hasn't.
1701 // In this case, CKKS shouldn't roll the TLK.
1702
1703 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1704
1705 // Spin up CKKS subsystem.
1706 [self startCKKSSubsystem];
1707 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // just to be sure it's fetched
1708
1709 // Items should upload.
1710 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1711 [self addGenericPassword: @"data" account: @"account-delete-me"];
1712 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1713
1714 [self waitForCKModifications];
1715
1716 // Bump the etag on the class C current key record, but don't change any data
1717 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
1718 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
1719 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
1720
1721 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
1722 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
1723
1724 // Add another item. This write should fail, then CKKS should recover without rolling the key hierarchy or issuing a fetch.
1725 self.silentFetchesAllowed = false;
1726 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1727 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1728 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
1729 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1730 }
1731
1732 - (void)testRecoverMultipleItemsFromIncrementedCurrentKeyPointerEtag {
1733 // CloudKit sometimes reports the current key pointers have changed (etag mismatch), but their content hasn't.
1734 // In this case, CKKS shouldn't roll the TLK.
1735 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1736
1737 // Spin up CKKS subsystem.
1738 [self startCKKSSubsystem];
1739 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // just to be sure it's fetched
1740
1741 // Items should upload.
1742 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1743 [self addGenericPassword: @"data" account: @"account-delete-me"];
1744 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1745
1746 [self waitForCKModifications];
1747
1748 // Bump the etag on the class C current key record, but don't change any data
1749 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
1750 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
1751 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
1752
1753 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
1754 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
1755
1756 // Add another item. This write should fail, then CKKS should recover without rolling the key hierarchy or issuing a fetch.
1757 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
1758 secnotice("ckks", "releasing outgoing-queue hold");
1759 }];
1760
1761 self.silentFetchesAllowed = false;
1762 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1763 [self expectCKModifyItemRecords:2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1764 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
1765 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
1766
1767 [self.operationQueue addOperation: self.keychainView.holdOutgoingQueueOperation];
1768 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1769 }
1770
1771 - (void)testOnboardOldItemsCreatingKeyHierarchy {
1772 // In this test, we'll check if the CKKS subsystem will pick up a keychain item which existed before the key hierarchy, both with and without a UUID attached at item creation
1773
1774 // Test starts with nothing in CloudKit, and CKKS blocked. Add one item without a UUID...
1775
1776 SecCKKSTestSetDisableAutomaticUUID(true);
1777 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1778
1779 // and an item with a UUID...
1780 SecCKKSTestSetDisableAutomaticUUID(false);
1781 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1782
1783 // We expect an upload of the key hierarchy
1784 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1785
1786 // We then expect an upload of the added items
1787 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1788
1789 [self startCKKSSubsystem];
1790
1791 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1792 }
1793
1794 - (void)testOnboardOldItemsWithExistingKeyHierarchy {
1795 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1796
1797 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1798 [self addGenericPassword: @"data" account: @"account-delete-me"];
1799
1800 [self startCKKSSubsystem];
1801 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1802 }
1803
1804 - (void)testOnboardOldItemsWithExistingKeyHierarchyExtantTLK {
1805 // Test starts key hierarchy in our fake CloudKit, the TLK arrived in the local keychain, and CKKS blocked.
1806 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1807 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1808 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1809
1810 // Add one item without a UUID...
1811 SecCKKSTestSetDisableAutomaticUUID(true);
1812 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1813
1814 // and an item with a UUID...
1815 SecCKKSTestSetDisableAutomaticUUID(false);
1816 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1817
1818 // Now that we have an item in the keychain, allow CKKS to spin up. It should upload the item, using the key hierarchy already extant
1819 // We expect a single record to be uploaded.
1820 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1821
1822 // Spin up CKKS subsystem.
1823 [self startCKKSSubsystem];
1824
1825 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1826 }
1827
1828 - (void)testOnboardOldItemsWithExistingKeyHierarchyLateTLK {
1829 // Test starts key hierarchy in our fake CloudKit, and CKKS blocked.
1830 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1831 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1832 self.keychainZone.flag = true;
1833
1834 // Add one item without a UUID...
1835 SecCKKSTestSetDisableAutomaticUUID(true);
1836 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1837
1838 // and an item with a UUID...
1839 SecCKKSTestSetDisableAutomaticUUID(false);
1840 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1841
1842 // Now that we have an item in the keychain, allow CKKS to spin up. It should upload the item, using the key hierarchy already extant
1843
1844 // Spin up CKKS subsystem.
1845 [self startCKKSSubsystem];
1846 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should have become waitfortlk");
1847
1848 // Now, save the TLK to the keychain (to simulate it coming in via SOS).
1849 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1850 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1851
1852 // We expect a single record to be uploaded.
1853 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1854
1855 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1856 XCTAssertTrue(self.keychainZone.flag, "Keychain zone shouldn't have been reset");
1857 }
1858
1859 - (void)testResync {
1860 // We need to set up a desynced situation to test our resync.
1861 // First, let CKKS start up and send several items to CloudKit (that we'll then desync!)
1862 __block NSError* error = nil;
1863
1864 // Test starts with keys in CloudKit (so we can create items later)
1865 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1866 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1867 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1868
1869 [self addGenericPassword: @"data" account: @"first"];
1870 [self addGenericPassword: @"data" account: @"second"];
1871 [self addGenericPassword: @"data" account: @"third"];
1872 [self addGenericPassword: @"data" account: @"fourth"];
1873 NSUInteger passwordCount = 4u;
1874
1875 [self checkGenericPassword: @"data" account: @"first"];
1876 [self checkGenericPassword: @"data" account: @"second"];
1877 [self checkGenericPassword: @"data" account: @"third"];
1878 [self checkGenericPassword: @"data" account: @"fourth"];
1879
1880 [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1881
1882 [self startCKKSSubsystem];
1883
1884 // Wait for uploads to happen
1885 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1886 [self waitForCKModifications];
1887 // One TLK share record
1888 XCTAssertEqual(self.keychainZone.currentDatabase.count, SYSTEM_DB_RECORD_COUNT+passwordCount+1, "Have SYSTEM_DB_RECORD_COUNT+passwordCount+1 objects in cloudkit");
1889
1890 // Now, corrupt away!
1891 // Extract all passwordCount items for Corruption
1892 NSArray<CKRecord*>* items = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
1893 XCTAssertEqual(items.count, passwordCount, "Have %lu Items in cloudkit", (unsigned long)passwordCount);
1894
1895 // For the first record, delete all traces of it from CKKS. But! it remains in local keychain.
1896 // Expected outcome: CKKS resyncs; item exists again.
1897 CKRecord* delete = items[0];
1898 NSString* deleteAccount = [[self decryptRecord: delete] objectForKey: (__bridge id) kSecAttrAccount];
1899 XCTAssertNotNil(deleteAccount, "received an account for the local delete object");
1900
1901 __weak __typeof(self) weakSelf = self;
1902 [self.keychainView dispatchSync:^bool{
1903 __strong __typeof(weakSelf) strongSelf = weakSelf;
1904 XCTAssertNotNil(strongSelf, "self exists");
1905
1906 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1907 if(ckme) {
1908 [ckme deleteFromDatabase: &error];
1909 }
1910 XCTAssertNil(error, "no error removing CKME");
1911 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1912 if(oqe) {
1913 [oqe deleteFromDatabase: &error];
1914 }
1915 XCTAssertNil(error, "no error removing OQE");
1916 CKKSIncomingQueueEntry* iqe = [CKKSIncomingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1917 if(iqe) {
1918 [iqe deleteFromDatabase: &error];
1919 }
1920 XCTAssertNil(error, "no error removing IQE");
1921 return true;
1922 }];
1923
1924 // For the second record, delete all traces of it from CloudKit.
1925 // Expected outcome: deleted locally
1926 CKRecord* remoteDelete = items[1];
1927 NSString* remoteDeleteAccount = [[self decryptRecord: remoteDelete] objectForKey: (__bridge id) kSecAttrAccount];
1928 XCTAssertNotNil(remoteDeleteAccount, "received an account for the remote delete object");
1929
1930 [self.keychainZone deleteCKRecordIDFromZone: remoteDelete.recordID];
1931 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
1932 [database removeObjectForKey: remoteDelete.recordID];
1933 }
1934
1935 // The third record gets modified in CloudKit, but not locally.
1936 // Expected outcome: use the CloudKit version
1937 CKRecord* remoteDataChanged = items[2];
1938 NSMutableDictionary* remoteDataDictionary = [[self decryptRecord: remoteDataChanged] mutableCopy];
1939 NSString* remoteDataChangedAccount = [remoteDataDictionary objectForKey: (__bridge id) kSecAttrAccount];
1940 XCTAssertNotNil(remoteDataChangedAccount, "Received an account for the remote-data-changed object");
1941 remoteDataDictionary[(__bridge id) kSecValueData] = [@"CloudKitWins" dataUsingEncoding: NSUTF8StringEncoding];
1942
1943 CKRecord* newData = [self newRecord: remoteDataChanged.recordID withNewItemData: remoteDataDictionary];
1944 [self.keychainZone addToZone: newData];
1945 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
1946 database[remoteDataChanged.recordID] = newData;
1947 }
1948
1949 // The fourth record stays in-sync. Good work, everyone!
1950 // Expected outcome: stays in-sync
1951 NSString* insyncAccount = [[self decryptRecord: items[3]] objectForKey: (__bridge id) kSecAttrAccount];
1952 XCTAssertNotNil(insyncAccount, "Received an account for the in-sync object");
1953
1954 // The fifth record gets magically added to CloudKit, but CKKS has never heard of it
1955 // (emulates a lost record on the client, but that CloudKit already believes it's sent the record for)
1956 // Expected outcome: added to local keychain
1957 NSString* remoteOnlyAccount = @"remote-only";
1958 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount: remoteOnlyAccount];
1959 [self.keychainZone addToZone: ckr];
1960 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
1961 database[ckr.recordID] = ckr;
1962 }
1963
1964 ckksnotice("ckksresync", self.keychainView, "local delete: %@ %@", delete.recordID.recordName, deleteAccount);
1965 ckksnotice("ckksresync", self.keychainView, "Remote deletion: %@ %@", remoteDelete.recordID.recordName, remoteDeleteAccount);
1966 ckksnotice("ckksresync", self.keychainView, "Remote data changed: %@ %@", remoteDataChanged.recordID.recordName, remoteDataChangedAccount);
1967 ckksnotice("ckksresync", self.keychainView, "in-sync: %@ %@", items[3].recordID.recordName, insyncAccount);
1968 ckksnotice("ckksresync", self.keychainView, "Remote only: %@ %@", ckr.recordID.recordName, remoteOnlyAccount);
1969
1970 CKKSSynchronizeOperation* resyncOperation = [self.keychainView resyncWithCloud];
1971 [resyncOperation waitUntilFinished];
1972
1973 XCTAssertNil(resyncOperation.error, "No error during the resync operation");
1974
1975 // Now do some checking. Remember, we don't know which record we corrupted, so use the parsed account variables to check.
1976
1977 [self findGenericPassword: deleteAccount expecting: errSecSuccess];
1978 [self findGenericPassword: remoteDeleteAccount expecting: errSecItemNotFound];
1979 [self findGenericPassword: remoteDataChangedAccount expecting: errSecSuccess];
1980 [self findGenericPassword: insyncAccount expecting: errSecSuccess];
1981 [self findGenericPassword: remoteOnlyAccount expecting: errSecSuccess];
1982
1983 [self checkGenericPassword: @"data" account: deleteAccount];
1984 //[self checkGenericPassword: @"data" account: remoteDeleteAccount];
1985 [self checkGenericPassword: @"CloudKitWins" account: remoteDataChangedAccount];
1986 [self checkGenericPassword: @"data" account: insyncAccount];
1987 [self checkGenericPassword: @"data" account: remoteOnlyAccount];
1988
1989 [self.keychainView dispatchSync:^bool{
1990 __strong __typeof(weakSelf) strongSelf = weakSelf;
1991 XCTAssertNotNil(strongSelf, "self exists");
1992
1993 CKKSMirrorEntry* ckme = nil;
1994
1995 ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1996 XCTAssertNil(error);
1997 XCTAssertNotNil(ckme);
1998
1999 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDelete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2000 XCTAssertNil(error);
2001 XCTAssertNil(ckme); // deleted!
2002
2003 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDataChanged.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2004 XCTAssertNil(error);
2005 XCTAssertNotNil(ckme);
2006
2007 ckme = [CKKSMirrorEntry tryFromDatabase:items[3].recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2008 XCTAssertNil(error);
2009 XCTAssertNotNil(ckme);
2010
2011 ckme = [CKKSMirrorEntry tryFromDatabase:ckr.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2012 XCTAssertNil(error);
2013 XCTAssertNotNil(ckme);
2014 return true;
2015 }];
2016 }
2017
2018 - (void)testResyncItemsMissingFromLocalKeychain {
2019 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2020
2021 // We want:
2022 // one password correctly synced between local keychain and CloudKit
2023 // one password incorrectly disappeared from local keychain, but in mirror table
2024 // one password sitting in the outgoing queue
2025 // one password sitting in the incoming queue
2026
2027 // Add and sync two passwords
2028 [self addGenericPassword: @"data" account: @"first"];
2029 [self addGenericPassword: @"data" account: @"second"];
2030
2031 [self checkGenericPassword: @"data" account: @"first"];
2032 [self checkGenericPassword: @"data" account: @"second"];
2033
2034 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2035 [self startCKKSSubsystem];
2036 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2037 [self waitForCKModifications];
2038 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2039
2040 // Now, place an item in the outgoing queue
2041
2042 //[self addGenericPassword: @"data" account: @"third"];
2043 //[self checkGenericPassword: @"data" account: @"third"];
2044
2045 // Now, corrupt away!
2046 // Extract all passwordCount items for Corruption
2047 NSArray<CKRecord*>* items = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
2048 XCTAssertEqual(items.count, 2u, "Have %lu Items in cloudkit", (unsigned long)2u);
2049
2050 // For the first record, surreptitiously remove from local keychain
2051 CKRecord* remove = items[0];
2052 NSString* removeAccount = [[self decryptRecord:remove] objectForKey:(__bridge id)kSecAttrAccount];
2053 XCTAssertNotNil(removeAccount, "received an account for the local delete object");
2054
2055 NSURL* kcpath = (__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)@"keychain-2-debug.db");
2056 sqlite3* db;
2057 sqlite3_open([[kcpath path] UTF8String], &db);
2058 NSString* query = [NSString stringWithFormat:@"DELETE FROM genp WHERE uuid=\"%@\"", remove.recordID.recordName];
2059 char* sqlerror = NULL;
2060 XCTAssertEqual(SQLITE_OK, sqlite3_exec(db, [query UTF8String], NULL, NULL, &sqlerror), "SQL deletion shouldn't error");
2061 XCTAssertTrue(sqlerror == NULL, "No error string should have been returned: %s", sqlerror);
2062 if(sqlerror) {
2063 sqlite3_free(sqlerror);
2064 sqlerror = NULL;
2065 }
2066 sqlite3_close(db);
2067
2068 // The second record is kept in-sync
2069
2070 // Now, add an in-flight change (for record 3)
2071 [self holdCloudKitModifications];
2072 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2073 [self addGenericPassword:@"data" account:@"third"];
2074 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2075
2076 // For the fourth, add a new record but prevent incoming queue processing
2077 self.keychainView.holdIncomingQueueOperation = [CKKSResultOperation named:@"hold-incoming" withBlock:^{}];
2078
2079 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"fourth"];
2080 [self.keychainZone addToZone:ckr];
2081 [self.keychainView notifyZoneChange:nil];
2082
2083 // Now, where are we....
2084 CKKSScanLocalItemsOperation* scanLocal = [self.keychainView scanLocalItems:@"test-scan"];
2085 [scanLocal waitUntilFinished];
2086
2087 XCTAssertEqual(scanLocal.missingLocalItemsFound, 1u, "Should have found one missing item");
2088
2089 // Allow everything to proceed
2090 [self releaseCloudKitModificationHold];
2091 [self.operationQueue addOperation:self.keychainView.holdIncomingQueueOperation];
2092
2093 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2094 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2095
2096 // And ensure that all four items are present again
2097 [self findGenericPassword: @"first" expecting: errSecSuccess];
2098 [self findGenericPassword: @"second" expecting: errSecSuccess];
2099 [self findGenericPassword: @"third" expecting: errSecSuccess];
2100 [self findGenericPassword: @"fourth" expecting: errSecSuccess];
2101 }
2102
2103 - (void)testResyncLocal {
2104 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2105 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2106 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2107
2108 [self addGenericPassword: @"data" account: @"first"];
2109 [self addGenericPassword: @"data" account: @"second"];
2110 NSUInteger passwordCount = 2u;
2111
2112 [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2113 [self startCKKSSubsystem];
2114
2115 // Wait for uploads to happen
2116 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2117 [self waitForCKModifications];
2118
2119 // Local resyncs shouldn't fetch clouds.
2120 self.silentFetchesAllowed = false;
2121 SecCKKSDisable();
2122 [self deleteGenericPassword:@"first"];
2123 [self deleteGenericPassword:@"second"];
2124 SecCKKSEnable();
2125
2126 // And they're gone!
2127 [self findGenericPassword:@"first" expecting:errSecItemNotFound];
2128 [self findGenericPassword:@"second" expecting:errSecItemNotFound];
2129
2130 CKKSLocalSynchronizeOperation* op = [self.keychainView resyncLocal];
2131 [op waitUntilFinished];
2132 XCTAssertNil(op.error, "Shouldn't be an error resyncing locally");
2133
2134 // And they're back!
2135 [self checkGenericPassword: @"data" account: @"first"];
2136 [self checkGenericPassword: @"data" account: @"second"];
2137 }
2138
2139 - (void)testPlistRestoreResyncsLocal {
2140 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2141 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2142 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2143
2144 [self addGenericPassword: @"data" account: @"first"];
2145 [self addGenericPassword: @"data" account: @"second"];
2146 NSUInteger passwordCount = 2u;
2147
2148 [self checkGenericPassword: @"data" account: @"first"];
2149 [self checkGenericPassword: @"data" account: @"second"];
2150
2151 [self expectCKModifyItemRecords:passwordCount currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2152 [self startCKKSSubsystem];
2153
2154 // Wait for uploads to happen
2155 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2156 [self waitForCKModifications];
2157
2158 // o no
2159 // This 'restores' a plist keychain backup
2160 // That will kick off a local resync in CKKS, so hold that until we're ready...
2161 self.keychainView.holdLocalSynchronizeOperation = [CKKSResultOperation named:@"hold-local-synchronize" withBlock:^{}];
2162
2163 // Local resyncs shouldn't fetch clouds.
2164 self.silentFetchesAllowed = false;
2165
2166 CFErrorRef cferror = NULL;
2167 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
2168 CFErrorRef cfcferror = NULL;
2169
2170 bool ret = SecServerImportKeychainInPlist(dbt, SecSecurityClientGet(), KEYBAG_NONE, KEYBAG_NONE,
2171 (__bridge CFDictionaryRef)@{}, kSecBackupableItemFilter, false, &cfcferror);
2172
2173 XCTAssertNil(CFBridgingRelease(cfcferror), "Shouldn't error importing a 'backup'");
2174 XCTAssert(ret, "Importing a 'backup' should have succeeded");
2175 return true;
2176 });
2177 XCTAssertNil(CFBridgingRelease(cferror), "Shouldn't error mucking about in the db");
2178
2179 // Restore is additive so original items stick around
2180 [self findGenericPassword:@"first" expecting:errSecSuccess];
2181 [self findGenericPassword:@"second" expecting:errSecSuccess];
2182
2183 // Allow the local resync to continue...
2184 [self.operationQueue addOperation:self.keychainView.holdLocalSynchronizeOperation];
2185 [self.keychainView waitForOperationsOfClass:[CKKSLocalSynchronizeOperation class]];
2186
2187 // Items are still here!
2188 [self checkGenericPassword: @"data" account: @"first"];
2189 [self checkGenericPassword: @"data" account: @"second"];
2190 }
2191
2192 - (void)testMultipleZoneAdd {
2193 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2194 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2195
2196 // Bring up a new zone: we expect a key hierarchy upload.
2197 [self.injectedManager findOrCreateView:(id)kSecAttrViewHintAppleTV];
2198 CKRecordZoneID* appleTVZoneID = [[CKRecordZoneID alloc] initWithZoneName:(__bridge NSString*) kSecAttrViewHintAppleTV ownerName:CKCurrentUserDefaultName];
2199 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:appleTVZoneID];
2200
2201 // We also expect the view manager's notifyNewTLKsInKeychain call to fire once (after some delay)
2202 OCMExpect([self.mockCKKSViewManager notifyNewTLKsInKeychain]);
2203
2204 // Let the horses loose
2205 [self startCKKSSubsystem];
2206
2207 // We expect a single record to be uploaded to the 'keychain' view
2208 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2209 [self addGenericPassword: @"data" account: @"account-delete-me"];
2210 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2211
2212 // We expect a single record to be uploaded to the 'atv' view
2213 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:appleTVZoneID];
2214 [self addGenericPassword: @"atv"
2215 account: @"tvaccount"
2216 viewHint:(__bridge NSString*) kSecAttrViewHintAppleTV
2217 access:(id)kSecAttrAccessibleAfterFirstUnlock
2218 expecting:errSecSuccess message:@"AppleTV view-hinted object"];
2219
2220 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2221
2222 OCMVerifyAllWithDelay(self.mockCKKSViewManager, 10);
2223 }
2224
2225 - (void)testMultipleZoneDelete {
2226 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2227 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2228
2229 [self startCKKSSubsystem];
2230
2231 // We expect a single record to be uploaded.
2232 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2233 [self addGenericPassword: @"data" account: @"account-delete-me"];
2234 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2235
2236 // Bring up a new zone: we expect a key hierarchy and an item.
2237 [self.injectedManager findOrCreateView:(id)kSecAttrViewHintAppleTV];
2238 CKRecordZoneID* appleTVZoneID = [[CKRecordZoneID alloc] initWithZoneName:(__bridge NSString*) kSecAttrViewHintAppleTV ownerName:CKCurrentUserDefaultName];
2239 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:appleTVZoneID];
2240 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:appleTVZoneID];
2241
2242 [self addGenericPassword: @"atv"
2243 account: @"tvaccount"
2244 viewHint:(__bridge NSString*) kSecAttrViewHintAppleTV
2245 access:(id)kSecAttrAccessibleAfterFirstUnlock
2246 expecting:errSecSuccess
2247 message:@"AppleTV view-hinted object"];
2248 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2249
2250 // We expect a single record to be deleted from the ATV zone
2251 [self expectCKDeleteItemRecords: 1 zoneID:appleTVZoneID];
2252 [self deleteGenericPassword:@"tvaccount"];
2253 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2254
2255 // Now we expect a single record to be deleted from the test zone
2256 [self expectCKDeleteItemRecords: 1 zoneID:self.keychainZoneID];
2257 [self deleteGenericPassword:@"account-delete-me"];
2258 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2259 }
2260
2261 - (void)testRestartWithoutRefetch {
2262 // Restarting the CKKS operation should check that it's been 15 minutes since the last fetch before it fetches again. Simulate this.
2263
2264 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2265 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2266 [self startCKKSSubsystem];
2267
2268 [self.keychainView waitForKeyHierarchyReadiness];
2269 [self waitForCKModifications];
2270 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2271
2272 // Tear down the CKKS object and disallow fetches
2273 [self.keychainView halt];
2274 self.silentFetchesAllowed = false;
2275
2276 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2277 [self.keychainView waitForKeyHierarchyReadiness];
2278 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2279
2280 // Okay, cool, rad, now let's set the date to be very long ago and check that there's positively a fetch
2281 [self.keychainView halt];
2282 self.silentFetchesAllowed = false;
2283
2284 [self.keychainView dispatchSync: ^bool {
2285 NSError* error = nil;
2286 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
2287
2288 XCTAssertNil(error, "no error pulling ckse from database");
2289 XCTAssertNotNil(ckse, "received a ckse");
2290
2291 ckse.lastFetchTime = [NSDate distantPast];
2292 [ckse saveToDatabase: &error];
2293 XCTAssertNil(error, "no error saving to database");
2294 return true;
2295 }];
2296
2297 [self expectCKFetch];
2298 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2299 [self.keychainView waitForKeyHierarchyReadiness];
2300 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2301 }
2302
2303 - (void)testRecoverFromZoneCreationFailure {
2304 // Fail the zone creation.
2305 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
2306 [self failNextZoneCreation:self.keychainZoneID];
2307
2308 // Spin up CKKS subsystem.
2309 [self startCKKSSubsystem];
2310
2311 // The CKKS subsystem should figure out the issue, and fix it.
2312 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2313
2314 [self.keychainView waitForKeyHierarchyReadiness];
2315 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2316
2317 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2318 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2319 [self addGenericPassword: @"data" account: @"account-delete-me"];
2320 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2321
2322 XCTAssertNil(self.zones[self.keychainZoneID].creationError, "Creation error was unset (and so CKKS probably dealt with the error");
2323 }
2324
2325 - (void)testRecoverFromZoneSubscriptionFailure {
2326 // Fail the zone subscription.
2327 [self failNextZoneSubscription:self.keychainZoneID];
2328
2329 // Spin up CKKS subsystem.
2330 [self startCKKSSubsystem];
2331
2332 // The CKKS subsystem should figure out the issue, and fix it.
2333 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2334
2335 [self.keychainView waitForKeyHierarchyReadiness];
2336 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2337
2338 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2339 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2340 [self addGenericPassword: @"data" account: @"account-delete-me"];
2341 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2342
2343 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
2344 }
2345
2346 - (void)testRecoverFromZoneSubscriptionFailureDueToZoneNotExisting {
2347 // This is different from testRecoverFromZoneSubscriptionFailure, since the zone is gone. CKKS must attempt to re-create the zone.
2348
2349 // Silently fail the zone creation
2350 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
2351 [self failNextZoneCreationSilently:self.keychainZoneID];
2352
2353 // Spin up CKKS subsystem.
2354 [self startCKKSSubsystem];
2355
2356 // The CKKS subsystem should figure out the issue, and fix it.
2357 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2358
2359 [self.keychainView waitForKeyHierarchyReadiness];
2360 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2361
2362 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2363 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2364 [self addGenericPassword: @"data" account: @"account-delete-me"];
2365 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2366
2367 XCTAssertFalse(self.zones[self.keychainZoneID].flag, "Zone flag was reset");
2368 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
2369 }
2370
2371 - (void)testRecoverFromDeletedTLKWithStashedTLK {
2372 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
2373
2374 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2375 NSError* error = nil;
2376
2377 // Stash the TLKs.
2378 [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
2379 XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
2380
2381 // And delete the non-stashed version
2382 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
2383 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
2384
2385 // Spin up CKKS subsystem.
2386 [self startCKKSSubsystem];
2387
2388 [self.keychainView waitForKeyHierarchyReadiness];
2389 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2390
2391 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2392 [self addGenericPassword: @"data" account: @"account-delete-me"];
2393 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2394
2395 // CKKS should recreate the syncable TLK.
2396 [self checkNSyncableTLKsInKeychain: 1];
2397 }
2398
2399 - (void)testRecoverFromDeletedTLKWithStashedTLKUponRestart {
2400 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
2401
2402 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2403 // Spin up CKKS subsystem.
2404 [self startCKKSSubsystem];
2405 [self.keychainView waitForKeyHierarchyReadiness];
2406
2407 // Tear down the CKKS object
2408 [self.keychainView halt];
2409
2410 NSError* error = nil;
2411
2412 // Stash the TLKs.
2413 [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
2414 XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
2415
2416 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
2417 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
2418
2419 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2420 [self.keychainView waitForKeyHierarchyReadiness];
2421 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2422
2423 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2424 [self addGenericPassword: @"data" account: @"account-delete-me"];
2425 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2426
2427 // CKKS should recreate the syncable TLK.
2428 [self checkNSyncableTLKsInKeychain: 1];
2429 }
2430
2431 - (void)testRecoverFromTLKWriteFailure {
2432 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
2433 // Test starts with nothing in CloudKit, and will fail the first TLK write.
2434 NSError* noNetwork = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}];
2435 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:noNetwork];
2436
2437 // Spin up CKKS subsystem.
2438 [self startCKKSSubsystem];
2439
2440 // The CKKS subsystem should figure out the issue, and fix it.
2441 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2442
2443 [self.keychainView waitForKeyHierarchyReadiness];
2444 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2445
2446 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2447 [self addGenericPassword: @"data" account: @"account-delete-me"];
2448 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2449
2450 // A network failure creating new TLKs shouldn't delete the 'failed' syncable one.
2451 [self checkNSyncableTLKsInKeychain: 2];
2452 }
2453
2454 - (void)testRecoverFromTLKRace {
2455 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
2456 // Test starts with nothing in CloudKit, and will fail the first TLK write.
2457 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject: ^{
2458 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2459 }];
2460
2461 // Spin up CKKS subsystem.
2462 [self startCKKSSubsystem];
2463
2464 // The first TLK write should fail, and then our fake TLKs should be there in CloudKit.
2465 // It shouldn't write anything back up to CloudKit.
2466 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2467
2468 // Now the TLKs arrive from the other device...
2469 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2470 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2471 [self.keychainView waitForKeyHierarchyReadiness];
2472
2473 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2474 [self addGenericPassword: @"data" account: @"account-delete-me"];
2475 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2476
2477 // A race failure creating new TLKs should delete the old syncable one.
2478 [self checkNSyncableTLKsInKeychain: 1];
2479 }
2480
2481 - (void)testRecoverFromNullCurrentKeyPointers {
2482 // The current key pointers in cloudkit shouldn't ever not exist if keys do. But, if they don't, CKKS must recover.
2483
2484 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
2485 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2486 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2487
2488 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
2489 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
2490 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2491 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2492 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2493
2494 // Spin up CKKS subsystem.
2495 [self startCKKSSubsystem];
2496
2497 // The CKKS subsystem should figure out the issue, and fix it.
2498 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
2499
2500 [self.keychainView waitForKeyHierarchyReadiness];
2501
2502 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2503 }
2504
2505 - (void)testRecoverFromNoCurrentKeyPointers {
2506 // The current key pointers in cloudkit shouldn't ever point to nil. But, if they do, CKKS must recover.
2507
2508 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
2509 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2510 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2511
2512 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
2513 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentTLKPointer.storedCKRecord.recordID], "Deleted TLK pointer from zone");
2514 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassAPointer.storedCKRecord.recordID], "Deleted class a pointer from zone");
2515 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassCPointer.storedCKRecord.recordID], "Deleted class c pointer from zone");
2516
2517 // Spin up CKKS subsystem.
2518 [self startCKKSSubsystem];
2519
2520 // The CKKS subsystem should figure out the issue, and fix it.
2521 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
2522
2523 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
2524
2525 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2526 }
2527
2528 - (void)testRecoverFromBadChangeTag {
2529 // We received a report where a machine appeared to have an up-to-date change tag, but was continuously attempting to create a new TLK hierarchy. No idea why.
2530
2531 // Test starts with a broken key hierarchy in our fake CloudKit, but a (incorrectly) up-to-date change tag stored locally.
2532 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2533 SecCKKSTestSetDisableKeyNotifications(true); // Don't tell CKKS about this key material; we're pretending like this is a securityd restart
2534 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2535 SecCKKSTestSetDisableKeyNotifications(false);
2536
2537 [self.keychainView dispatchSync: ^bool {
2538 NSError* error = nil;
2539 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainZoneID.zoneName];
2540 XCTAssertNotNil(ckse, "should have received a ckse");
2541
2542 ckse.ckzonecreated = true;
2543 ckse.ckzonesubscribed = true;
2544 ckse.changeToken = self.keychainZone.currentChangeToken;
2545
2546 [ckse saveToDatabase: &error];
2547 XCTAssertNil(error, "shouldn't have gotten an error saving to database");
2548 return true;
2549 }];
2550
2551 // The CKKS subsystem should try to write TLKs, but fail. It'll then upload a TLK share for the keys already in CloudKit
2552 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
2553 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2554
2555 // Spin up CKKS subsystem.
2556 [self startCKKSSubsystem];
2557 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2558
2559 // CKKS should then happily use the keys in CloudKit
2560 [self createClassCItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me"];
2561 [self createClassAItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me-class-a"];
2562 }
2563
2564 - (void)testRecoverFromDeletedKeysNewItem {
2565 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2566 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2567
2568 [self startCKKSSubsystem];
2569 [self.keychainView waitForKeyHierarchyReadiness];
2570
2571 // We expect a single class C record to be uploaded.
2572 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2573 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2574
2575 [self addGenericPassword: @"data" account: @"account-delete-me"];
2576 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2577
2578 [self waitForCKModifications];
2579 [self.keychainView waitUntilAllOperationsAreFinished];
2580
2581 // Now, delete the local keys from the keychain (but leave the synced TLK)
2582 SecCKKSTestSetDisableKeyNotifications(true);
2583 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2584 (id)kSecClass : (id)kSecClassInternetPassword,
2585 (id)kSecAttrNoLegacy : @YES,
2586 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2587 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
2588 }), @"Deleting local keys");
2589 SecCKKSTestSetDisableKeyNotifications(false);
2590
2591 NSError* error = nil;
2592 [self.keychainZoneKeys.classC loadKeyMaterialFromKeychain:&error];
2593 XCTAssertNotNil(error, "Error loading class C key material from keychain");
2594
2595 // We expect a single class C record to be uploaded.
2596 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2597 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2598
2599 [self addGenericPassword: @"datadata" account: @"account-no-keys"];
2600 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2601
2602 // We expect a single class A record to be uploaded.
2603 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2604 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2605 [self addGenericPassword:@"asdf"
2606 account:@"account-class-A"
2607 viewHint:nil
2608 access:(id)kSecAttrAccessibleWhenUnlocked
2609 expecting:errSecSuccess
2610 message:@"Adding class A item"];
2611 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2612 }
2613
2614 - (void)testRecoverFromDeletedKeysReceive {
2615 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2616 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2617
2618 [self startCKKSSubsystem];
2619 [self.keychainView waitForKeyHierarchyReadiness];
2620
2621 [self waitForCKModifications];
2622 [self.keychainView waitUntilAllOperationsAreFinished];
2623
2624 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2625
2626 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
2627
2628 // Now, delete the local keys from the keychain (but leave the synced TLK)
2629 SecCKKSTestSetDisableKeyNotifications(true);
2630 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2631 (id)kSecClass : (id)kSecClassInternetPassword,
2632 (id)kSecAttrNoLegacy : @YES,
2633 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2634 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
2635 }), @"Deleting local keys");
2636 SecCKKSTestSetDisableKeyNotifications(false);
2637
2638 // Trigger a notification (with hilariously fake data)
2639 [self.keychainZone addToZone: ckr];
2640 [self.keychainView notifyZoneChange:nil];
2641 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2642 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2643
2644 [self findGenericPassword: @"account0" expecting:errSecSuccess];
2645 }
2646
2647 - (void)testRecoverDeletedTLK {
2648 // If the TLK disappears halfway through, well, that's no good. But we should recover using TLK sharing
2649
2650 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2651 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2652
2653 [self startCKKSSubsystem];
2654 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
2655
2656 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2657 [self waitForCKModifications];
2658
2659 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
2660 [self.keychainView waitUntilAllOperationsAreFinished];
2661
2662 // Now, delete the local keys from the keychain
2663 SecCKKSTestSetDisableKeyNotifications(true);
2664 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2665 (id)kSecClass : (id)kSecClassInternetPassword,
2666 (id)kSecAttrNoLegacy : @YES,
2667 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2668 (id)kSecAttrSynchronizable : (id)kSecAttrSynchronizableAny,
2669 }), @"Deleting CKKS keys");
2670 SecCKKSTestSetDisableKeyNotifications(false);
2671
2672 // Trigger a notification (with hilariously fake data)
2673 [self.keychainZone addToZone: ckr];
2674 [self.keychainView notifyZoneChange:nil];
2675
2676 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2677
2678 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should return to 'ready'");
2679
2680 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // Do this again, to allow for non-atomic key state machinery switching
2681
2682 [self findGenericPassword: @"account0" expecting:errSecSuccess];
2683 }
2684
2685 - (void)testRecoverMissingRolledKey {
2686 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2687
2688 NSString* accountShouldExist = @"under-rolled-key";
2689 NSString* accountWillExist = @"under-rolled-key-later";
2690 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountShouldExist];
2691 [self.keychainZone addToZone: ckr];
2692
2693 CKRecord* ckrAddedLater = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountWillExist];
2694 CKKSKey* pastClassCKey = self.keychainZoneKeys.classC;
2695
2696 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2697 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2698
2699 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2700
2701 [self startCKKSSubsystem];
2702 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
2703
2704 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2705 [self waitForCKModifications];
2706
2707 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2708 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2709 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
2710
2711 // Now, find and delete the class C key that ckrAddedLater is under
2712 NSError* error = nil;
2713 XCTAssertTrue([pastClassCKey deleteKeyMaterialFromKeychain:&error], "Should be able to delete old key material from keychain");
2714 XCTAssertNil(error, "Should be no error deleting old key material from keychain");
2715
2716 [self.keychainZone addToZone:ckrAddedLater];
2717 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2718
2719 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2720 [self findGenericPassword:accountWillExist expecting:errSecSuccess];
2721
2722 XCTAssertTrue([pastClassCKey loadKeyMaterialFromKeychain:&error], "Class C key should be back in the keychain");
2723 XCTAssertNil(error, "Should be no error loading key from keychain");
2724 }
2725
2726 - (void)testRecoverMissingRolledClassAKeyWhileLocked {
2727 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2728
2729 NSString* accountShouldExist = @"under-rolled-key";
2730 NSString* accountWillExist = @"under-rolled-key-later";
2731 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountShouldExist key:self.keychainZoneKeys.classA];
2732 [self.keychainZone addToZone: ckr];
2733
2734 CKRecord* ckrAddedLater = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountWillExist key:self.keychainZoneKeys.classA];
2735 CKKSKey* pastClassAKey = self.keychainZoneKeys.classA;
2736
2737 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2738 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2739
2740 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2741
2742 [self startCKKSSubsystem];
2743 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
2744
2745 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2746 [self waitForCKModifications];
2747
2748 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2749 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2750 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
2751
2752 // Now, find and delete the class C key that ckrAddedLater is under
2753 NSError* error = nil;
2754 XCTAssertTrue([pastClassAKey deleteKeyMaterialFromKeychain:&error], "Should be able to delete old key material from keychain");
2755 XCTAssertNil(error, "Should be no error deleting old key material from keychain");
2756
2757 // now, lock the keychain
2758 self.aksLockState = true;
2759 [self.lockStateTracker recheck];
2760
2761 [self.keychainZone addToZone:ckrAddedLater];
2762 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2763
2764 // Item should still not exist due to the lock state....
2765 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2766 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
2767
2768 self.aksLockState = false;
2769 [self.lockStateTracker recheck];
2770
2771 // And now it does
2772 [self.keychainView waitUntilAllOperationsAreFinished];
2773 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2774 [self findGenericPassword:accountWillExist expecting:errSecSuccess];
2775
2776 XCTAssertTrue([pastClassAKey loadKeyMaterialFromKeychain:&error], "Class A key should be back in the keychain");
2777 XCTAssertNil(error, "Should be no error loading key from keychain");
2778 }
2779
2780 - (void)testRecoverFromBadCurrentKeyPointer {
2781 // The current key pointers in cloudkit shouldn't ever point to missing entries. But, if they do, CKKS must recover.
2782
2783 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
2784 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2785 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2786
2787 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
2788 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
2789 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real tlk" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
2790 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class a key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
2791 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class c key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
2792
2793 // Spin up CKKS subsystem.
2794 [self startCKKSSubsystem];
2795
2796 // The CKKS subsystem should figure out the issue, and fix it (while uploading itself a TLK Share)
2797 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2798
2799 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
2800
2801 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2802 }
2803
2804 - (void)testRecoverFromIncorrectCurrentTLKPointer {
2805 // The current key pointers in cloudkit shouldn't ever point to wrong entries. But, if they do, CKKS must recover.
2806
2807 // Test starts with a rolled hierarchy, and CKPs pointing to the wrong items
2808 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2809 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2810
2811 CKKSCurrentKeyPointer* oldTLKCKP = self.keychainZoneKeys.currentTLKPointer;
2812 CKRecord* oldTLKPointer = [self.keychainZone.currentDatabase[self.keychainZoneKeys.currentTLKPointer.storedCKRecord.recordID] copy];
2813
2814 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2815 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2816
2817 ZoneKeys* newZoneKeys = [self.keychainZoneKeys copy];
2818
2819 // And put the oldTLKPointer back
2820 [self.zones[self.keychainZoneID] addToZone:oldTLKPointer];
2821 self.keychainZoneKeys.currentTLKPointer = oldTLKCKP;
2822
2823 // Make sure it stuck:
2824 XCTAssertNotEqualObjects(self.keychainZoneKeys.currentTLKPointer,
2825 newZoneKeys.currentTLKPointer,
2826 "current TLK pointer should now not point to proper TLK");
2827
2828 // Spin up CKKS subsystem.
2829 [self startCKKSSubsystem];
2830
2831 // The CKKS subsystem should figure out the issue, and fix it (while uploading itself a TLK Share)
2832 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
2833
2834 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
2835
2836 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2837 [self waitForCKModifications];
2838
2839 XCTAssertEqualObjects(self.keychainZoneKeys.currentTLKPointer,
2840 newZoneKeys.currentTLKPointer,
2841 "current TLK pointer should now point to proper TLK");
2842 XCTAssertEqualObjects(self.keychainZoneKeys.currentClassAPointer,
2843 newZoneKeys.currentClassAPointer,
2844 "current Class A pointer should now point to proper Class A key");
2845 XCTAssertEqualObjects(self.keychainZoneKeys.currentClassCPointer,
2846 newZoneKeys.currentClassCPointer,
2847 "current Class C pointer should now point to proper Class C key");
2848 }
2849
2850 - (void)testRecoverFromCloudKitFetchFail {
2851 // Test starts with nothing in database, but one in our fake CloudKit.
2852 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2853 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
2854
2855 // The first two CKRecordZoneChanges should fail with a 'network unavailable' error.
2856 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
2857 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
2858
2859 // Spin up CKKS subsystem.
2860 [self startCKKSSubsystem];
2861
2862 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2863 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2864 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2865
2866 // We expect a single record to be uploaded
2867 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2868 [self addGenericPassword: @"data" account: @"account-delete-me"];
2869 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2870
2871 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2872 [self addGenericPassword:@"asdf"
2873 account:@"account-class-A"
2874 viewHint:nil
2875 access:(id)kSecAttrAccessibleWhenUnlocked
2876 expecting:errSecSuccess
2877 message:@"Adding class A item"];
2878 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2879 }
2880
2881 - (void)testRecoverFromCloudKitFetchNetworkFailAfterReady {
2882 // Test starts with nothing in database, but one in our fake CloudKit.
2883 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2884
2885 // Spin up CKKS subsystem.
2886 [self startCKKSSubsystem];
2887
2888 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
2889 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateReady, "CKKS entered ready");
2890
2891 // Network is unavailable
2892 self.reachabilityFlags = 0;
2893 [self.reachabilityTracker recheck];
2894
2895 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
2896 [self.keychainZone addToZone:ckr];
2897
2898 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
2899
2900 // Say network is available
2901 self.reachabilityFlags = kSCNetworkReachabilityFlagsReachable;
2902 [self.reachabilityTracker recheck];
2903
2904 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2905
2906 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
2907 }
2908
2909 - (void)testRecoverFromCloudKitFetchNetworkFailBeforeReady {
2910 // Test starts with nothing in database, but one in our fake CloudKit.
2911 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2912
2913 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
2914 [self.keychainZone addToZone:ckr];
2915
2916 // Network is unavailable
2917 self.reachabilityFlags = 0;
2918 [self.reachabilityTracker recheck];
2919
2920 // Spin up CKKS subsystem.
2921 [self startCKKSSubsystem];
2922
2923 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateInitializing] wait:20*NSEC_PER_SEC], "CKKS entered initializing");
2924 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateInitializing, "CKKS entered initializing");
2925
2926 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2927 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2928 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2929
2930 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
2931
2932 // Say network is available
2933 self.reachabilityFlags = kSCNetworkReachabilityFlagsReachable;
2934 [self.reachabilityTracker recheck];
2935
2936 [self.keychainView waitUntilAllOperationsAreFinished];
2937 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2938
2939 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
2940 }
2941
2942 - (void)testWaitAfterCloudKitNetworkFailDuringOutgoingQueueOperation {
2943 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2944
2945 [self startCKKSSubsystem];
2946
2947 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered ready");
2948
2949 // Network is now unavailable
2950 self.reachabilityFlags = 0;
2951 [self.reachabilityTracker recheck];
2952
2953 NSError* noNetwork = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}];
2954 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:noNetwork];
2955 [self addGenericPassword: @"data" account: @"account-delete-me"];
2956
2957 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2958 sleep(2);
2959
2960 // Once network is available again, the write should happen
2961 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
2962 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2963
2964 self.reachabilityFlags = kSCNetworkReachabilityFlagsReachable;
2965 [self.reachabilityTracker recheck];
2966
2967 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
2968
2969 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2970 }
2971
2972 - (void)testRecoverFromCloudKitFetchFailWithDelay {
2973 // Test starts with nothing in database, but one in our fake CloudKit.
2974 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2975 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
2976
2977 // The first CKRecordZoneChanges should fail with a 'delay' error.
2978 self.silentFetchesAllowed = false;
2979 [self.keychainZone failNextFetchWith:[[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorRequestRateLimited userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:4]}]];
2980 [self expectCKFetch];
2981
2982 // Spin up CKKS subsystem.
2983 [self startCKKSSubsystem];
2984
2985 // Ensure it doesn't fetch within these three seconds (if it does, an exception will throw).
2986 sleep(3);
2987
2988 // Okay, you can fetch again.
2989 [self expectCKFetch];
2990
2991 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2992 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2993 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2994
2995 // We expect a single record to be uploaded
2996 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2997 [self addGenericPassword: @"data" account: @"account-delete-me"];
2998 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2999 }
3000
3001 - (void)testRecoverFromCloudKitOldChangeToken {
3002 // Test starts with nothing in database, but one in our fake CloudKit.
3003 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3004
3005 // Spin up CKKS subsystem.
3006 [self startCKKSSubsystem];
3007
3008 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3009 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3010 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3011
3012 // We expect a single record to be uploaded
3013 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3014 [self addGenericPassword: @"data" account: @"account-delete-me"];
3015 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3016
3017 // Delete all old database states, to destroy the change tag validity
3018 [self.keychainZone.pastDatabases removeAllObjects];
3019
3020 // We expect a total local flush and refetch
3021 self.silentFetchesAllowed = false;
3022 [self expectCKFetch]; // one to fail with a CKErrorChangeTokenExpired error
3023 [self expectCKFetch]; // and one to succeed
3024
3025 // Trigger a fake change notification
3026 [self.keychainView notifyZoneChange:nil];
3027
3028 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3029
3030 // And check that a new upload happens just fine.
3031 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3032 [self addGenericPassword:@"asdf"
3033 account:@"account-class-A"
3034 viewHint:nil
3035 access:(id)kSecAttrAccessibleWhenUnlocked
3036 expecting:errSecSuccess
3037 message:@"Adding class A item"];
3038 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3039 }
3040
3041 - (void)testRecoverFromCloudKitUnknownDeviceStateRecord {
3042 // Test starts with nothing in database, but one in our fake CloudKit.
3043 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3044 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3045 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3046
3047 // Save a new device state record with some fake etag
3048 [self.keychainView dispatchSync: ^bool {
3049 CKKSDeviceStateEntry* cdse = [[CKKSDeviceStateEntry alloc] initForDevice:self.ckDeviceID
3050 osVersion:@"fake-record"
3051 lastUnlockTime:[NSDate date]
3052 octagonPeerID:nil
3053 octagonStatus:nil
3054 circlePeerID:self.circlePeerID
3055 circleStatus:kSOSCCInCircle
3056 keyState:SecCKKSZoneKeyStateWaitForTLK
3057 currentTLKUUID:nil
3058 currentClassAUUID:nil
3059 currentClassCUUID:nil
3060 zoneID:self.keychainZoneID
3061 encodedCKRecord:nil];
3062 XCTAssertNotNil(cdse, "Should have created a fake CDSE");
3063 CKRecord* record = [cdse CKRecordWithZoneID:self.keychainZoneID];
3064 XCTAssertNotNil(record, "Should have created a fake CDSE CKRecord");
3065 record.etag = @"fake etag";
3066 cdse.storedCKRecord = record;
3067
3068 NSError* error = nil;
3069 [cdse saveToDatabase:&error];
3070 XCTAssertNil(error, @"No error saving cdse to database");
3071
3072 return true;
3073 }];
3074
3075 // Spin up CKKS subsystem.
3076 [self startCKKSSubsystem];
3077
3078 // We expect a record failure, since the device state record is broke
3079 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
3080
3081 // And then we expect a clean write
3082 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
3083 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3084
3085 [self addGenericPassword: @"data" account: @"account-delete-me"];
3086 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3087 }
3088
3089 - (void)testRecoverFromCloudKitUnknownItemRecord {
3090 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3091
3092 // Spin up CKKS subsystem.
3093 [self startCKKSSubsystem];
3094
3095 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3096
3097 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
3098 [self.keychainZone addToZone:ckr];
3099
3100 [self.keychainView notifyZoneChange:nil];
3101 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3102
3103 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3104
3105 // Delete the record from CloudKit, but miss the notification
3106 XCTAssertNil([self.keychainZone deleteCKRecordIDFromZone: ckr.recordID], "Deleting the record from fake CloudKit should succeed");
3107
3108 // Expect a failed upload when we modify the item
3109 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
3110 [self updateGenericPassword:@"never seen again" account:@"account-delete-me"];
3111 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3112
3113 [self.keychainView waitUntilAllOperationsAreFinished];
3114
3115 // And the item should be disappeared from the local keychain
3116 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3117 }
3118
3119 - (void)testRecoverFromCloudKitUserDeletedZone {
3120 // Test starts with nothing in database, but one in our fake CloudKit.
3121 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3122
3123 // Spin up CKKS subsystem.
3124 [self startCKKSSubsystem];
3125
3126 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3127 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3128 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3129
3130 // We expect a single record to be uploaded
3131 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3132 [self addGenericPassword: @"data" account: @"account-delete-me"];
3133 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3134
3135 // The first CKRecordZoneChanges should fail with a 'CKErrorUserDeletedZone' error. This will cause a local reset, ending up with zone re-creation.
3136 self.zones[self.keychainZoneID] = nil; // delete the zone
3137 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorUserDeletedZone userInfo:@{}]];
3138
3139 // We expect CKKS to recreate the zone, then perform a key hierarchy upload, and then the class C item upload
3140 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3141 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3142
3143 [self.keychainView notifyZoneChange:nil];
3144
3145 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3146
3147 // And check that a new upload occurs.
3148 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3149
3150 [self addGenericPassword:@"asdf"
3151 account:@"account-class-A"
3152 viewHint:nil
3153 access:(id)kSecAttrAccessibleWhenUnlocked
3154 expecting:errSecSuccess
3155 message:@"Adding class A item"];
3156 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3157 }
3158
3159 - (void)testRecoverFromCloudKitZoneNotFoundWithoutZoneDeletion {
3160 // Test starts with nothing in database, but one in our fake CloudKit.
3161 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3162 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
3163
3164 // Spin up CKKS subsystem.
3165 [self startCKKSSubsystem];
3166
3167 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3168 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3169 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3170
3171 // We expect a single record to be uploaded
3172 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3173 [self addGenericPassword: @"data" account: @"account-delete-me"];
3174 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3175
3176 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
3177
3178 [self waitForCKModifications];
3179 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
3180
3181 // The next CKRecordZoneChanges will fail with a 'zone not found' error.
3182 self.zones[self.keychainZoneID] = nil; // delete the zone
3183
3184 // We expect CKKS to reset itself and recover, then a key hierarchy upload, and then the class C item upload
3185 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3186 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3187
3188 [self.keychainView notifyZoneChange:nil];
3189 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3190 [self waitForCKModifications];
3191
3192 // And check that a new upload occurs.
3193 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3194
3195 [self addGenericPassword:@"asdf"
3196 account:@"account-class-A"
3197 viewHint:nil
3198 access:(id)kSecAttrAccessibleWhenUnlocked
3199 expecting:errSecSuccess
3200 message:@"Adding class A item"];
3201 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3202 }
3203
3204 - (void)testRecoverFromCloudKitZoneNotFoundFetchBeforeSigninOccurs {
3205 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
3206
3207 // Before CKKS sign-in, it receives a fetch rpc
3208 XCTestExpectation *fetchReturns = [self expectationWithDescription:@"fetch returned"];
3209 [self.injectedManager rpcFetchAndProcessChanges:nil reply:^(NSError *result) {
3210 XCTAssertNil(result, "Should be no error fetching and processing changes");
3211 [fetchReturns fulfill];
3212 }];
3213
3214 // start 'login'. CKKS Should upload a key hierarchy
3215 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
3216 [self startCKKSSubsystem];
3217
3218 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
3219
3220 // We expect a single record to be uploaded
3221 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
3222 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3223 [self addGenericPassword: @"data" account: @"account-delete-me"];
3224 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3225
3226 // The fetch should have come back by now
3227 [self waitForExpectations: @[fetchReturns] timeout:5];
3228 }
3229
3230 - (void)testNoCloudKitAccount {
3231 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
3232 self.accountStatus = CKAccountStatusNoAccount;
3233 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCNotInCircle error:nil];;
3234 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3235
3236 self.silentFetchesAllowed = false;
3237 [self startCKKSSubsystem];
3238
3239 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3240
3241 [self addGenericPassword: @"data" account: @"account-delete-me"];
3242 [self.keychainView waitUntilAllOperationsAreFinished];
3243
3244 // simulate a NSNotification callback (but still logged out)
3245 self.accountStatus = CKAccountStatusNoAccount;
3246 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3247
3248 // There should be no further uploads, even when we save keychain items
3249 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3250 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3251
3252 [self.keychainView waitUntilAllOperationsAreFinished];
3253 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3254
3255 // Test that there are no items in the database (since we never logged in)
3256 [self checkNoCKKSData: self.keychainView];
3257 }
3258
3259 - (void)testSACloudKitAccount {
3260 // Test starts with nothing in database and the user logged into CloudKit and in circle, but the account is not HSA2.
3261 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];
3262 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3263
3264 self.accountStatus = CKAccountStatusAvailable;
3265 self.supportsDeviceToDeviceEncryption = NO;
3266
3267 self.silentFetchesAllowed = false;
3268 [self startCKKSSubsystem];
3269
3270 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3271 XCTAssertNotNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's no account");
3272 XCTAssertEqualObjects(self.accountStateTracker.currentAccountError.domain, CKKSErrorDomain, "Account tracker error should be in CKKSErrorDomain");
3273 XCTAssertEqual(self.accountStateTracker.currentAccountError.code, CKKSNotHSA2, "Account tracker error should be upset about HSA2");
3274
3275 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3276
3277 // There should be no uploads, even when we save keychain items and enter/exit circle
3278 [self addGenericPassword: @"data" account: @"account-delete-me"];
3279 [self.keychainView waitUntilAllOperationsAreFinished];
3280
3281 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCNotInCircle error:nil];
3282 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3283 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3284
3285 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];
3286 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3287 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3288
3289 [self.keychainView waitUntilAllOperationsAreFinished];
3290 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3291
3292 // Test that there are no items in the database (since we never were in an HSA2 account)
3293 [self checkNoCKKSData: self.keychainView];
3294 }
3295
3296 - (void)testNoCircle {
3297 // Test starts with nothing in database and the user logged into CloudKit, but out of Circle.
3298 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCNotInCircle error:nil];
3299 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3300
3301 self.accountStatus = CKAccountStatusAvailable;
3302
3303 self.silentFetchesAllowed = false;
3304 [self startCKKSSubsystem];
3305
3306 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3307 XCTAssertNotNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's no account");
3308 XCTAssertEqualObjects(self.accountStateTracker.currentAccountError.domain, (__bridge NSString*)kSOSErrorDomain, "Account tracker error should be in SOSErrorDomain");
3309 XCTAssertEqual(self.accountStateTracker.currentAccountError.code, kSOSErrorNotInCircle, "Account tracker error should be upset about out-of-circle");
3310
3311 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3312
3313 [self addGenericPassword: @"data" account: @"account-delete-me"];
3314 [self.keychainView waitUntilAllOperationsAreFinished];
3315
3316 // simulate a NSNotification callback (but still logged out)
3317 self.accountStatus = CKAccountStatusNoAccount;
3318 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3319
3320 // There should be no further uploads, even when we save keychain items
3321 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3322 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3323
3324 [self.keychainView waitUntilAllOperationsAreFinished];
3325 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3326
3327 // Test that there are no items in the database (since we never logged in)
3328 [self checkNoCKKSData: self.keychainView];
3329 }
3330
3331 - (void)testCloudKitLogin {
3332 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
3333 self.accountStatus = CKAccountStatusNoAccount;
3334 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCNotInCircle error:nil];
3335 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3336
3337 // Before we inform CKKS of its account state....
3338 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
3339
3340 [self startCKKSSubsystem];
3341
3342 XCTAssertEqual(0, [self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], "Should have been told of a 'logout' event on startup");
3343 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event shouldn't have happened");
3344 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3345
3346 [self.keychainView waitUntilAllOperationsAreFinished];
3347 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3348
3349 XCTAssertNotNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's no account");
3350 XCTAssertEqualObjects(self.accountStateTracker.currentAccountError.domain, CKKSErrorDomain, "Account tracker error should be in CKKSErrorDomain");
3351 XCTAssertEqual(self.accountStateTracker.currentAccountError.code, CKKSNotLoggedIn, "Account tracker error should just be 'no account'");
3352
3353 // simulate a cloudkit login and NSNotification callback
3354 self.accountStatus = CKAccountStatusAvailable;
3355 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3356
3357 XCTAssertNotNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's no account");
3358 XCTAssertEqualObjects(self.accountStateTracker.currentAccountError.domain, (__bridge NSString*)kSOSErrorDomain, "Account tracker error should be in SOSErrorDomain");
3359 XCTAssertEqual(self.accountStateTracker.currentAccountError.code, kSOSErrorNotInCircle, "Account tracker error should be upset about out-of-circle");
3360
3361 // No writes yet, since we're not in circle
3362 [self.keychainView waitUntilAllOperationsAreFinished];
3363 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3364
3365 // We expect some sort of TLK/key hierarchy upload once we are notified of entering the circle.
3366 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3367
3368 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];
3369 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3370
3371 XCTAssertNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's an account");
3372
3373 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3374 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3375 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3376
3377 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3378 [self waitForCKModifications];
3379
3380 // We expect a single class C record to be uploaded.
3381 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3382 [self addGenericPassword: @"data" account: @"account-delete-me"];
3383
3384 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3385 [self waitForCKModifications];
3386 }
3387
3388 - (void)testCloudKitLogoutLogin {
3389 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
3390 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3391
3392 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
3393 [self startCKKSSubsystem];
3394 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3395 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3396 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3397
3398 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3399 [self waitForCKModifications];
3400
3401 // We expect a single class C record to be uploaded.
3402 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3403 [self addGenericPassword: @"data" account: @"account-delete-me"];
3404
3405 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3406 [self waitForCKModifications];
3407
3408 // simulate a cloudkit logout and NSNotification callback
3409 self.accountStatus = CKAccountStatusNoAccount;
3410 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3411 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCNotInCircle error:nil];
3412 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3413
3414 XCTAssertNotNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's no account");
3415 XCTAssertEqualObjects(self.accountStateTracker.currentAccountError.domain, CKKSErrorDomain, "Account tracker error should be in CKKSErrorDomain");
3416 XCTAssertEqual(self.accountStateTracker.currentAccountError.code, CKKSNotLoggedIn, "Account tracker error should just believe we're not logged in");
3417
3418 // Test that there are no items in the database after logout
3419 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
3420 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
3421 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3422 [self checkNoCKKSData: self.keychainView];
3423
3424 // There should be no further uploads, even when we save keychain items
3425 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3426 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3427
3428 [self.keychainView waitUntilAllOperationsAreFinished];
3429 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3430 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
3431
3432 // simulate a cloudkit login
3433 // We should expect CKKS to re-find the key hierarchy it already uploaded, and then send up the two records we added during the pause
3434 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
3435
3436 self.accountStatus = CKAccountStatusAvailable;
3437 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3438 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];
3439 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3440 XCTAssertNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's an account");
3441
3442 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3443 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3444 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3445
3446 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3447
3448 // Let everything settle...
3449 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3450 [self waitForCKModifications];
3451
3452 // Logout again
3453 self.accountStatus = CKAccountStatusNoAccount;
3454 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3455 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCNotInCircle error:nil];
3456 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3457
3458 // Test that there are no items in the database after logout
3459 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
3460 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
3461 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3462 [self checkNoCKKSData: self.keychainView];
3463
3464 // There should be no further uploads, even when we save keychain items
3465 [self addGenericPassword: @"data" account: @"account-delete-me-5"];
3466 [self addGenericPassword: @"data" account: @"account-delete-me-6"];
3467
3468 [self.keychainView waitUntilAllOperationsAreFinished];
3469 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3470
3471 // simulate a cloudkit login
3472 // We should expect CKKS to re-find the key hierarchy it already uploaded, and then send up the two records we added during the pause
3473 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
3474
3475 self.accountStatus = CKAccountStatusAvailable;
3476 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3477 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];
3478 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3479
3480 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3481 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3482 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3483
3484 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3485
3486 // Let everything settle...
3487 [self.keychainView waitUntilAllOperationsAreFinished];
3488 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3489
3490 // Logout again
3491 self.accountStatus = CKAccountStatusNoAccount;
3492 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3493 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCNotInCircle error:nil];
3494 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3495
3496 // Test that there are no items in the database after logout
3497 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
3498 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
3499 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3500 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
3501 [self checkNoCKKSData: self.keychainView];
3502
3503 // Force zone into error state
3504 self.keychainView.keyHierarchyState = SecCKKSZoneKeyStateError;
3505
3506 self.accountStatus = CKAccountStatusAvailable;
3507 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3508 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];
3509 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3510
3511 XCTestExpectation *operationRun = [self expectationWithDescription:@"operation run"];
3512 NSOperation* op = [NSBlockOperation named:@"test" withBlock:^{
3513 [operationRun fulfill];
3514 }];
3515
3516 [op addDependency:self.keychainView.keyStateReadyDependency];
3517 [self.operationQueue addOperation:op];
3518
3519 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3520 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3521 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3522
3523 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3524 [self waitForExpectations: @[operationRun] timeout:5];
3525 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3526 }
3527
3528 - (void)testCloudKitLogoutDueToGreyMode {
3529 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
3530 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
3531
3532 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
3533 [self startCKKSSubsystem];
3534 XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "Should have been told of a 'login'");
3535 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:50*NSEC_PER_MSEC], "'logout' event should be reset");
3536 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3537
3538 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
3539
3540 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3541 [self waitForCKModifications];
3542
3543 // simulate a cloudkit grey mode switch and NSNotification callback. CKKS should treat this as a logout
3544 self.iCloudHasValidCredentials = false;
3545 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3546
3547 XCTAssertNotNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's no account");
3548 XCTAssertEqualObjects(self.accountStateTracker.currentAccountError.domain, CKKSErrorDomain, "Account tracker error should be in CKKSErrorDomain");
3549 XCTAssertEqual(self.accountStateTracker.currentAccountError.code, CKKSiCloudGreyMode, "Account tracker error should be upset about grey mode");
3550
3551 // Test that there are no items in the database after logout
3552 XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "Should have been told of a 'logout'");
3553 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:50*NSEC_PER_MSEC], "'login' event should be reset");
3554 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3555 [self checkNoCKKSData: self.keychainView];
3556 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
3557
3558 // There should be no further uploads, even when we save keychain items
3559 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3560 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3561
3562 [self.keychainView waitUntilAllOperationsAreFinished];
3563 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3564
3565 // Also, fetches shouldn't occur
3566 self.silentFetchesAllowed = false;
3567 NSOperation* op = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
3568 CKKSResultOperation* timeoutOp = [CKKSResultOperation named:@"timeout" withBlock:^{}];
3569 [timeoutOp addDependency:op];
3570 [timeoutOp timeout:4*NSEC_PER_SEC];
3571 [self.operationQueue addOperation:timeoutOp];
3572 [timeoutOp waitUntilFinished];
3573
3574 // CloudKit figures its life out. We expect the two passwords from before to be uploaded
3575 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
3576 self.silentFetchesAllowed = true;
3577 self.iCloudHasValidCredentials = true;
3578 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3579
3580 XCTAssertNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's an account");
3581
3582 XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "Should have been told of a 'login'");
3583 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:50*NSEC_PER_MSEC], "'logout' event should be reset");
3584 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3585 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3586
3587 // And fetching still works!
3588 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D00" withAccount:@"account0"]];
3589 [self.keychainView notifyZoneChange:nil];
3590 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3591 [self findGenericPassword: @"account0" expecting:errSecSuccess];
3592 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3593 }
3594
3595 - (void)testCloudKitLoginRace {
3596 // Test starts with nothing in database, and 'in circle', but securityd hasn't received notification if we're logged into CloudKit.
3597 // CKKS should not call handleLogout.
3598
3599 id partialKVMock = OCMPartialMock(self.keychainView);
3600 OCMReject([partialKVMock handleCKLogout]);
3601 // note: don't unblock the ck account state object yet...
3602
3603 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];
3604 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3605
3606 // Add a keychain item, but make sure it doesn't upload yet.
3607 [self addGenericPassword: @"data" account: @"account-delete-me"];
3608
3609 [self.keychainView waitUntilAllOperationsAreFinished];
3610 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3611
3612 // Now that we're here (and handleCKLogout hasn't been called), bring the account up
3613
3614 // We expect some sort of TLK/key hierarchy upload once we are notified of entering the circle.
3615 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3616
3617 // We expect a single class C record to be uploaded.
3618 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3619
3620 self.accountStatus = CKAccountStatusAvailable;
3621 [self startCKAccountStatusMock];
3622
3623 // simulate another NSNotification callback
3624 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3625
3626 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3627 [self waitForCKModifications];
3628
3629 // Make sure new items upload too
3630 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3631 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3632 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3633
3634 [self.keychainView waitUntilAllOperationsAreFinished];
3635 [self waitForCKModifications];
3636 [self.keychainView halt];
3637
3638 [partialKVMock stopMocking];
3639 }
3640
3641 - (void)testDontLogOutIfBeforeFirstUnlock {
3642 // test starts as if a previously logged-in device has just rebooted
3643 self.aksLockState = true;
3644 self.accountStatus = CKAccountStatusAvailable;
3645
3646 // This is the original state of the account tracker
3647 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCError error:nil];
3648 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3649
3650 // And this is what the first circle status fetch will actually return
3651 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCError error:[NSError errorWithDomain:(__bridge id)kSOSErrorDomain code:kSOSErrorNotReady description:@"fake error: device is locked, so SOS doesn't know if it's in-circle"]];
3652 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3653
3654 XCTAssertNil(self.accountStateTracker.currentAccountError, "Account tracker error should not yet exist");
3655 XCTAssertEqual(self.accountStateTracker.currentComputedAccountStatus, CKKSAccountStatusUnknown, "Account tracker status should just be 'no account'");
3656 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKKS shouldn't know the account state yet");
3657
3658 [self startCKKSSubsystem];
3659
3660 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "Shouldn't have been told of a 'logout' event on startup");
3661 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event shouldn't have happened");
3662 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKKS shouldn't know the account state yet");
3663
3664 // And assume another CK status change
3665 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3666 XCTAssertEqual(self.accountStateTracker.currentComputedAccountStatus, CKKSAccountStatusUnknown, "Account tracker status should just be 'no account'");
3667 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKKS shouldn't know the account state yet");
3668
3669 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3670
3671 self.aksLockState = false;
3672 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];
3673 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3674
3675 XCTAssertNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's an account");
3676
3677 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3678 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3679 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3680
3681 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3682 [self waitForCKModifications];
3683
3684 // We expect a single class C record to be uploaded.
3685 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3686 [self addGenericPassword: @"data" account: @"account-delete-me"];
3687
3688 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3689 [self waitForCKModifications];
3690 }
3691
3692 - (void)testSyncableItemsAddedWhileLoggedOut {
3693 // Test that once CKKS is up and 'logged out', nothing happens when syncable items are added
3694 self.accountStatus = CKAccountStatusNoAccount;
3695 [self startCKAccountStatusMock];
3696
3697 XCTAssertEqual([self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], 0, "CKKS should be told that it's logged out");
3698
3699 // CKKS shouldn't decide to poke its state machine, but it should still send the notification
3700 XCTestExpectation* viewChangeNotification = [self expectChangeForView:self.keychainZoneID.zoneName];
3701
3702 // Reject all attempts to trigger a state machine update
3703 id pokeKeyStateMachineScheduler = OCMClassMock([CKKSNearFutureScheduler class]);
3704 OCMReject([pokeKeyStateMachineScheduler trigger]);
3705 self.keychainView.pokeKeyStateMachineScheduler = pokeKeyStateMachineScheduler;
3706
3707 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3708
3709 [self waitForExpectations:@[viewChangeNotification] timeout:8];
3710 [pokeKeyStateMachineScheduler stopMocking];
3711 }
3712
3713
3714 - (void)testNotStuckAfterReset {
3715 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3716
3717 XCTestExpectation *operationRun = [self expectationWithDescription:@"operation run"];
3718 NSOperation* op = [NSBlockOperation named:@"test" withBlock:^{
3719 [operationRun fulfill];
3720 }];
3721
3722 [op addDependency:self.keychainView.keyStateReadyDependency];
3723 [self.operationQueue addOperation:op];
3724
3725 // And handle a spurious logout
3726 [self.keychainView handleCKLogout];
3727
3728 [self startCKKSSubsystem];
3729
3730 [self waitForExpectations: @[operationRun] timeout:20];
3731 }
3732
3733 - (void)testCKKSControlBringup {
3734 NSXPCInterface *interface = CKKSSetupControlProtocol([NSXPCInterface interfaceWithProtocol:@protocol(CKKSControlProtocol)]);
3735 XCTAssertNotNil(interface, "Received a configured CKKS interface");
3736 }
3737
3738 @end
3739
3740 #endif // OCTAGON