2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
27 #import <CloudKit/CloudKit.h>
28 #import <Foundation/NSXPCConnection_Private.h>
29 #import <XCTest/XCTest.h>
30 #import <OCMock/OCMock.h>
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>
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"
55 #import "keychain/ckks/tests/MockCloudKit.h"
57 #import "keychain/ckks/tests/CKKSTests.h"
60 @interface CKKSLockStateTracker ()
61 @property (nullable) NSDate* lastUnlockedTime;
64 @implementation CloudKitKeychainSyncingTests
68 - (void)testBringupToKeyStateReady {
69 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
70 [self startCKKSSubsystem];
72 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:4*NSEC_PER_SEC], @"Key state should have arrived at ready");
76 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
78 // We expect a single record to be uploaded.
79 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
81 [self startCKKSSubsystem];
82 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should have arrived at ready");
84 [self addGenericPassword: @"data" account: @"account-delete-me"];
86 OCMVerifyAllWithDelay(self.mockDatabase, 8);
89 - (void)testActiveTLKS {
90 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
92 // We expect a single record to be uploaded.
93 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
95 [self startCKKSSubsystem];
97 [self addGenericPassword: @"data" account: @"account-delete-me"];
99 OCMVerifyAllWithDelay(self.mockDatabase, 8);
101 NSDictionary<NSString *,NSString *>* tlks = [[CKKSViewManager manager] activeTLKs];
103 XCTAssertEqual([tlks count], (NSUInteger)1, "One TLK");
104 XCTAssertNotNil(tlks[@"keychain"], "keychain have a UUID");
108 - (void)testAddMultipleItems {
109 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
110 [self startCKKSSubsystem];
112 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
113 [self addGenericPassword: @"data" account: @"account-delete-me"];
114 OCMVerifyAllWithDelay(self.mockDatabase, 8);
116 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
117 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
118 OCMVerifyAllWithDelay(self.mockDatabase, 8);
120 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
121 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
122 OCMVerifyAllWithDelay(self.mockDatabase, 8);
125 - (void)testAddItemWithoutUUID {
126 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
127 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
128 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
129 [self saveTLKMaterialToKeychain:self.keychainZoneID];
131 [self startCKKSSubsystem];
133 [self.keychainView waitUntilAllOperationsAreFinished];
135 SecCKKSTestSetDisableAutomaticUUID(true);
136 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
138 SecCKKSTestSetDisableAutomaticUUID(false);
140 // We then expect an upload of the added item
141 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
143 OCMVerifyAllWithDelay(self.mockDatabase, 8);
146 - (void)testModifyItem {
147 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
149 NSString* account = @"account-delete-me";
151 [self startCKKSSubsystem];
153 // We expect a single record to be uploaded.
154 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
155 [self addGenericPassword: @"data" account: account];
156 OCMVerifyAllWithDelay(self.mockDatabase, 8);
158 // And then modified.
159 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
160 [self updateGenericPassword: @"otherdata" account:account];
161 OCMVerifyAllWithDelay(self.mockDatabase, 8);
164 - (void)testModifyItemImmediately {
165 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
166 NSString* account = @"account-delete-me";
168 [self startCKKSSubsystem];
169 [self holdCloudKitModifications];
171 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
172 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
173 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
174 [self addGenericPassword: @"data" account: account];
175 OCMVerifyAllWithDelay(self.mockDatabase, 8);
177 // Right now, the write in CloudKit is pending. Make the local modification...
178 [self updateGenericPassword: @"otherdata" account:account];
180 // And then schedule the update
181 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
182 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
183 [self releaseCloudKitModificationHold];
185 OCMVerifyAllWithDelay(self.mockDatabase, 8);
188 - (void)testModifyItemPrimaryKey {
189 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
191 NSString* account = @"account-delete-me";
193 [self startCKKSSubsystem];
195 // We expect a single record to be uploaded.
196 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
197 [self addGenericPassword: @"data" account: account];
198 OCMVerifyAllWithDelay(self.mockDatabase, 8);
200 // And then modified. Since we're changing the "primary key", we expect to delete the old record and upload a new one.
201 [self expectCKModifyItemRecords:1 deletedRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:nil];
202 [self updateAccountOfGenericPassword: @"new-account-delete-me" account:account];
203 OCMVerifyAllWithDelay(self.mockDatabase, 8);
206 - (void)testModifyItemDuringReencrypt {
207 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
208 NSString* account = @"account-delete-me";
210 [self startCKKSSubsystem];
211 [self holdCloudKitModifications];
213 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
214 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
215 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
216 [self addGenericPassword: @"data" account: account];
217 OCMVerifyAllWithDelay(self.mockDatabase, 8);
219 // Right now, the write in CloudKit is pending. Make the local modification...
220 [self updateGenericPassword: @"otherdata" account:account];
222 // And then schedule the update
223 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
224 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
226 // Stop the reencrypt operation from happening
227 self.keychainView.holdReencryptOutgoingItemsOperation = [CKKSGroupOperation named:@"reencrypt-hold" withBlock: ^{
228 secnotice("ckks", "releasing reencryption hold");
231 // The cloudkit operation finishes, letting the next OQO proceed (and set up the reencryption operation)
232 [self releaseCloudKitModificationHold];
234 // And wait for this to finish...
235 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
236 // And once more to quiesce.
237 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
239 // Pause outgoing queue operations to ensure the reencryption operation runs first
240 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
241 secnotice("ckks", "releasing outgoing-queue hold");
244 // Run the reencrypt items operation to completion.
245 [self.operationQueue addOperation: self.keychainView.holdReencryptOutgoingItemsOperation];
246 [self.keychainView waitForOperationsOfClass:[CKKSReencryptOutgoingItemsOperation class]];
248 [self.operationQueue addOperation: self.keychainView.holdOutgoingQueueOperation];
250 OCMVerifyAllWithDelay(self.mockDatabase, 8);
251 [self.keychainView waitUntilAllOperationsAreFinished];
252 [self waitForCKModifications];
255 - (void)testModifyItemBeforeReencrypt {
256 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
257 NSString* account = @"account-delete-me";
259 [self startCKKSSubsystem];
260 [self holdCloudKitModifications];
262 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
263 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
264 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
265 [self addGenericPassword: @"data" account: account];
266 OCMVerifyAllWithDelay(self.mockDatabase, 8);
268 // Right now, the write in CloudKit is pending. Make the local modification...
269 [self updateGenericPassword: @"otherdata" account:account];
271 // And then schedule the update, but for the final version of the password
272 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
273 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"third"]];
275 // Stop the reencrypt operation from happening
276 self.keychainView.holdReencryptOutgoingItemsOperation = [CKKSGroupOperation named:@"reencrypt-hold" withBlock: ^{
277 secnotice("ckks", "releasing reencryption hold");
280 // The cloudkit operation finishes, letting the next OQO proceed (and set up the reencryption operation)
281 [self releaseCloudKitModificationHold];
283 // And wait for this to finish...
284 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
285 // And once more to quiesce.
286 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
288 [self updateGenericPassword: @"third" account:account];
290 // Item should upload.
291 OCMVerifyAllWithDelay(self.mockDatabase, 8);
293 // Run the reencrypt items operation to completion.
294 [self.operationQueue addOperation: self.keychainView.holdReencryptOutgoingItemsOperation];
295 [self.keychainView waitForOperationsOfClass:[CKKSReencryptOutgoingItemsOperation class]];
297 [self.keychainView waitUntilAllOperationsAreFinished];
298 [self waitForCKModifications];
301 - (void)testModifyItemDuringNetworkFailure {
302 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
303 NSString* account = @"account-delete-me";
305 [self startCKKSSubsystem];
306 [self holdCloudKitModifications];
308 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
309 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
311 [self addGenericPassword: @"data" account: account];
312 OCMVerifyAllWithDelay(self.mockDatabase, 8);
314 // Right now, the write in CloudKit is pending. Make the local modification...
315 [self updateGenericPassword: @"otherdata" account:account];
317 // And then schedule the update, but for the final version of the password
318 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
319 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
321 // The cloudkit operation finishes, letting the next OQO proceed (and set up uploading the new item)
322 [self releaseCloudKitModificationHold];
324 // Item should upload.
325 OCMVerifyAllWithDelay(self.mockDatabase, 8);
327 [self.keychainView waitUntilAllOperationsAreFinished];
328 [self waitForCKModifications];
331 - (void)testOutgoingQueueRecoverFromStaleInflightEntry {
332 // CKKS is restarting with an existing in-flight OQE
333 // Note that this test is incomplete, and doesn't re-add the item to the local keychain
334 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
335 NSString* account = @"fake-account";
337 [self.keychainView dispatchSync:^bool {
338 NSError* error = nil;
340 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
342 CKKSItem* item = [self newItem:ckrid withNewItemData:[self fakeRecordDictionary:account zoneID:self.keychainZoneID] key:self.keychainZoneKeys.classC];
343 XCTAssertNotNil(item, "Should be able to create a new fake item");
345 CKKSOutgoingQueueEntry* oqe = [[CKKSOutgoingQueueEntry alloc] initWithCKKSItem:item action:SecCKKSActionAdd state:SecCKKSStateInFlight waitUntil:nil accessGroup:@"ckks"];
346 XCTAssertNotNil(oqe, "Should be able to create a new fake OQE");
347 [oqe saveToDatabase:&error];
349 XCTAssertNil(error, "Shouldn't error saving new OQE to database");
353 NSError *error = NULL;
354 XCTAssertEqual([CKKSOutgoingQueueEntry countByState:SecCKKSStateInFlight zone:self.keychainZoneID error:&error], 1,
355 "Expected on inflight entry in outgoing queue: %@", error);
357 // When CKKS restarts, it should find and re-upload this item
358 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
359 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
361 [self startCKKSSubsystem];
362 [self.keychainView waitForFetchAndIncomingQueueProcessing];
364 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
365 [self.keychainView waitForKeyHierarchyReadiness];
366 OCMVerifyAllWithDelay(self.mockDatabase, 8);
369 - (void)testOutgoingQueueRecoverFromNetworkFailure {
370 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
371 NSString* account = @"account-delete-me";
373 [self startCKKSSubsystem];
374 [self holdCloudKitModifications];
376 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
378 NSError* greyMode = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNotAuthenticated userInfo:@{}];
379 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:greyMode];
381 [self addGenericPassword: @"data" account: account];
382 OCMVerifyAllWithDelay(self.mockDatabase, 8);
384 // And then schedule the retried update
385 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
386 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
388 // The cloudkit operation finishes, letting the next OQO proceed (and set up uploading the new item)
389 [self releaseCloudKitModificationHold];
391 OCMVerifyAllWithDelay(self.mockDatabase, 8);
393 [self.keychainView waitUntilAllOperationsAreFinished];
394 [self waitForCKModifications];
397 - (void)testDeleteItem {
398 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
400 [self startCKKSSubsystem];
402 // We expect a single record to be uploaded.
403 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
404 [self addGenericPassword: @"data" account: @"account-delete-me"];
405 OCMVerifyAllWithDelay(self.mockDatabase, 8);
407 // We expect a single record to be deleted.
408 [self expectCKDeleteItemRecords: 1 zoneID:self.keychainZoneID];
409 [self deleteGenericPassword:@"account-delete-me"];
410 OCMVerifyAllWithDelay(self.mockDatabase, 8);
413 - (void)testDeleteItemImmediatelyAfterModify {
414 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
415 NSString* account = @"account-delete-me";
417 [self startCKKSSubsystem];
419 // We expect a single record to be uploaded.
420 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
421 [self addGenericPassword: @"data" account: account];
422 OCMVerifyAllWithDelay(self.mockDatabase, 8);
424 // Now, hold the modify
425 [self holdCloudKitModifications];
427 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
428 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
429 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
431 [self updateGenericPassword: @"otherdata" account:account];
432 OCMVerifyAllWithDelay(self.mockDatabase, 8);
434 // Right now, the write in CloudKit is pending. Make the local deletion...
435 [self deleteGenericPassword:account];
437 // And then schedule the update
438 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
439 [self releaseCloudKitModificationHold];
441 OCMVerifyAllWithDelay(self.mockDatabase, 8);
444 - (void)testDeleteItemAfterFetchAfterModify {
445 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
446 NSString* account = @"account-delete-me";
448 [self startCKKSSubsystem];
450 // We expect a single record to be uploaded.
451 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
452 [self addGenericPassword: @"data" account: account];
453 OCMVerifyAllWithDelay(self.mockDatabase, 8);
455 // Now, hold the modify
456 //[self holdCloudKitModifications];
458 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
459 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
460 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
462 [self updateGenericPassword: @"otherdata" account:account];
463 OCMVerifyAllWithDelay(self.mockDatabase, 8);
465 // Right now, the write in CloudKit is pending. Place a hold on outgoing queue processing
466 // Place a hold on processing the outgoing queue.
467 CKKSResultOperation* blockOutgoing = [CKKSResultOperation operationWithBlock:^{
468 secnotice("ckks", "Outgoing queue hold released.");
470 blockOutgoing.name = @"outgoing-queue-hold";
471 CKKSResultOperation* outgoingQueueOperation = [self.keychainView processOutgoingQueueAfter:blockOutgoing ckoperationGroup:nil];
473 [self deleteGenericPassword:account];
475 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
477 // Release the CK modification hold
478 //[self releaseCloudKitModificationHold];
481 [self.keychainView waitForFetchAndIncomingQueueProcessing];
482 [self.operationQueue addOperation:blockOutgoing];
483 [outgoingQueueOperation waitUntilFinished];
485 OCMVerifyAllWithDelay(self.mockDatabase, 8);
489 - (void)testReceiveItem {
490 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
491 [self startCKKSSubsystem];
493 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
494 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
495 (id)kSecAttrAccount : @"account-delete-me",
496 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
497 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
500 CFTypeRef item = NULL;
501 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
503 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
504 [self.keychainZone addToZone: ckr];
506 // Trigger a notification (with hilariously fake data)
507 [self.keychainView notifyZoneChange:nil];
509 [self.keychainView waitForFetchAndIncomingQueueProcessing];
510 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
513 - (void)testReceiveManyItems {
514 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
515 [self startCKKSSubsystem];
517 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D00" withAccount:@"account0"]];
518 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D01" withAccount:@"account1"]];
519 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D02" withAccount:@"account2"]];
520 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D03" withAccount:@"account3"]];
521 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D04" withAccount:@"account4"]];
522 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D05" withAccount:@"account5"]];
523 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D06" withAccount:@"account6"]];
524 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D07" withAccount:@"account7"]];
525 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D08" withAccount:@"account8"]];
526 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D09" withAccount:@"account9"]];
527 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D10" withAccount:@"account10"]];
528 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D11" withAccount:@"account11"]];
530 // Trigger a notification (with hilariously fake data)
531 [self.keychainView notifyZoneChange:nil];
533 [self.keychainView waitForFetchAndIncomingQueueProcessing];
535 [self findGenericPassword: @"account0" expecting:errSecSuccess];
536 [self findGenericPassword: @"account1" expecting:errSecSuccess];
537 [self findGenericPassword: @"account2" expecting:errSecSuccess];
538 [self findGenericPassword: @"account3" expecting:errSecSuccess];
539 [self findGenericPassword: @"account4" expecting:errSecSuccess];
540 [self findGenericPassword: @"account5" expecting:errSecSuccess];
541 [self findGenericPassword: @"account6" expecting:errSecSuccess];
542 [self findGenericPassword: @"account7" expecting:errSecSuccess];
543 [self findGenericPassword: @"account8" expecting:errSecSuccess];
544 [self findGenericPassword: @"account9" expecting:errSecSuccess];
545 [self findGenericPassword: @"account10" expecting:errSecSuccess];
546 [self findGenericPassword: @"account11" expecting:errSecSuccess];
549 - (void)testReceiveCollidingItem {
550 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
551 [self startCKKSSubsystem];
553 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
554 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
555 (id)kSecAttrAccount : @"account-delete-me",
556 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
557 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
560 CFTypeRef item = NULL;
561 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
563 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
564 CKRecord* ckr2 = [self createFakeRecord: self.keychainZoneID recordName: @"F9C58D31-7B59-481E-98AC-5A507ACB2D85"];
566 [self.keychainZone addToZone: ckr];
567 [self.keychainZone addToZone: ckr2];
569 // We expect a delete operation with the "higher" UUID.
570 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
572 // Trigger a notification (with hilariously fake data)
573 [self.keychainView notifyZoneChange:nil];
575 OCMVerifyAllWithDelay(self.mockDatabase, 6);
576 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
578 [self waitForCKModifications];
579 XCTAssertNil(self.keychainZone.currentDatabase[ckr2.recordID], "Correct record was deleted from CloudKit");
582 -(void)testReceiveItemDelete {
583 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
584 [self startCKKSSubsystem];
586 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
587 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
588 (id)kSecAttrAccount : @"account-delete-me",
589 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
590 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
593 CFTypeRef item = NULL;
594 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
596 [self.keychainView waitForFetchAndIncomingQueueProcessing];
598 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
599 [self.keychainZone addToZone: ckr];
601 // Trigger a notification (with hilariously fake data)
602 [self.keychainView notifyZoneChange:nil];
603 [self.keychainView waitForFetchAndIncomingQueueProcessing];
605 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
609 [self.keychainZone deleteCKRecordIDFromZone: [ckr recordID]];
610 [self.keychainView notifyZoneChange:nil];
611 [self.keychainView waitForFetchAndIncomingQueueProcessing];
613 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
616 -(void)testReceiveItemPhantomDelete {
617 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
618 [self startCKKSSubsystem];
620 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
621 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
622 (id)kSecAttrAccount : @"account-delete-me",
623 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
624 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
627 CFTypeRef item = NULL;
628 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
630 [self.keychainView waitForFetchAndIncomingQueueProcessing];
632 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
633 [self.keychainZone addToZone: ckr];
635 // Trigger a notification (with hilariously fake data)
636 [self.keychainView notifyZoneChange:nil];
637 [self.keychainView waitForFetchAndIncomingQueueProcessing];
639 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
642 [self.keychainView waitUntilAllOperationsAreFinished];
645 [self.keychainZone deleteCKRecordIDFromZone: [ckr recordID]];
647 // and add another, incorrect IQE
648 [self.keychainView dispatchSync: ^bool {
649 // Inefficient, but hey, it works
650 CKRecord* record = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-FFFF-FFFF-5A507ACB2D85"];
651 CKKSItem* fakeItem = [[CKKSItem alloc] initWithCKRecord: record];
653 CKKSIncomingQueueEntry* iqe = [[CKKSIncomingQueueEntry alloc] initWithCKKSItem:fakeItem
654 action:SecCKKSActionDelete
655 state:SecCKKSStateNew];
656 XCTAssertNotNil(iqe, "could create fake IQE");
657 NSError* error = nil;
658 XCTAssert([iqe saveToDatabase: &error], "Saved fake IQE to database");
659 XCTAssertNil(error, "No error saving fake IQE to database");
663 [self.keychainView notifyZoneChange:nil];
664 [self.keychainView waitForFetchAndIncomingQueueProcessing];
666 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
668 // The incoming queue should be empty
669 [self.keychainView dispatchSync: ^bool {
670 NSError* error = nil;
671 NSArray* iqes = [CKKSIncomingQueueEntry all:&error];
672 XCTAssertNil(error, "No error loading IQEs");
673 XCTAssertNotNil(iqes, "Could load IQEs");
674 XCTAssertEqual(iqes.count, 0u, "Incoming queue is empty");
678 -(void)testReceiveConflictOnJustAddedItem {
679 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
680 [self startCKKSSubsystem];
682 [self.keychainView waitForKeyHierarchyReadiness];
683 [self.keychainView waitUntilAllOperationsAreFinished];
685 // Place a hold on processing the outgoing queue.
686 CKKSResultOperation* blockOutgoing = [CKKSResultOperation operationWithBlock:^{
687 secnotice("ckks", "Outgoing queue hold released.");
689 blockOutgoing.name = @"outgoing-queue-hold";
690 CKKSResultOperation* outgoingQueueOperation = [self.keychainView processOutgoingQueueAfter:blockOutgoing ckoperationGroup:nil];
692 CKKSResultOperation* blockIncoming = [CKKSResultOperation operationWithBlock:^{
693 secnotice("ckks", "Incoming queue hold released.");
695 blockIncoming.name = @"incoming-queue-hold";
696 CKKSResultOperation* incomingQueueOperation = [self.keychainView processIncomingQueue:false after: blockIncoming];
698 [self addGenericPassword:@"localchange" account:@"account-delete-me"];
700 // Pull out the new item's UUID.
701 __block NSString* itemUUID = nil;
702 [self.keychainView dispatchSync:^bool {
703 NSError* error = nil;
704 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID ?: [[CKRecordZoneID alloc] initWithZoneName:@"keychain"
705 ownerName:CKCurrentUserDefaultName]
707 XCTAssertNil(error, "no error fetching uuids");
708 XCTAssertEqual(uuids.count, 1u, "There's exactly one outgoing queue entry");
711 XCTAssertNotNil(itemUUID, "Have a UUID for our new item");
715 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName: itemUUID]];
717 [self.keychainView notifyZoneChange:nil];
718 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
720 // Allow the outgoing queue operation to proceed
721 [self.operationQueue addOperation:blockOutgoing];
722 [outgoingQueueOperation waitUntilFinished];
724 // Allow the incoming queue operation to proceed
725 [self.operationQueue addOperation:blockIncoming];
726 [incomingQueueOperation waitUntilFinished];
728 [self checkGenericPassword:@"data" account:@"account-delete-me"];
730 [self.keychainView waitUntilAllOperationsAreFinished];
733 - (void)testReceiveCloudKitConflictOnJustAddedItems {
734 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
735 [self startCKKSSubsystem];
737 [self.keychainView waitForKeyHierarchyReadiness];
738 [self.keychainView waitUntilAllOperationsAreFinished];
740 // Place a hold on processing the outgoing queue.
741 self.keychainView.holdOutgoingQueueOperation = [CKKSResultOperation named:@"outgoing-queue-hold" withBlock:^{
742 secnotice("ckks", "Outgoing queue hold released.");
745 [self addGenericPassword:@"localchange" account:@"account-delete-me"];
747 // Pull out the new item's UUID.
748 __block NSString* itemUUID = nil;
749 [self.keychainView dispatchSync:^bool {
750 NSError* error = nil;
751 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID ?: [[CKRecordZoneID alloc] initWithZoneName:@"keychain"
752 ownerName:CKCurrentUserDefaultName]
754 XCTAssertNil(error, "no error fetching uuids");
755 XCTAssertEqual(uuids.count, 1u, "There's exactly one outgoing queue entry");
758 XCTAssertNotNil(itemUUID, "Have a UUID for our new item");
762 // Add a second item: this item should be uploaded after the failure of the first item
763 [self addGenericPassword:@"localchange" account:@"account-delete-me-2"];
765 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName: itemUUID]];
767 // Also, this write will increment the class C current pointer's etag
768 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
769 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
770 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
771 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
772 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
774 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
775 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
776 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
778 // Allow the outgoing queue operation to proceed
779 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
781 OCMVerifyAllWithDelay(self.mockDatabase, 8);
782 [self.keychainView waitUntilAllOperationsAreFinished];
784 [self checkGenericPassword:@"data" account:@"account-delete-me"];
785 [self checkGenericPassword:@"localchange" account:@"account-delete-me-2"];
789 -(void)testReceiveUnknownField {
790 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
792 [self startCKKSSubsystem];
793 [self.keychainView waitForKeyHierarchyReadiness];
795 NSError* error = nil;
797 // Manually encrypt an item
798 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
799 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
800 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
801 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
802 parentKeyUUID:self.keychainZoneKeys.classA.uuid
803 zoneID:recordID.zoneID];
804 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classA error:&error];
805 XCTAssertNotNil(itemkey, "Got a key");
806 cipheritem.wrappedkey = itemkey.wrappedkey;
807 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
809 NSData* future_data_field = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding];
810 NSString* future_string_field = @"authstring";
811 NSString* future_server_field = @"server_can_change_at_any_time";
812 NSNumber* future_number_field = [NSNumber numberWithInt:30];
814 // Use version 2, so future fields will be authenticated
815 cipheritem.encver = CKKSItemEncryptionVersion2;
816 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
818 authenticatedData[@"future_data_field"] = future_data_field;
819 authenticatedData[@"future_string_field"] = [future_string_field dataUsingEncoding:NSUTF8StringEncoding];
821 uint64_t n = OSSwapHostToLittleConstInt64([future_number_field unsignedLongValue]);
822 authenticatedData[@"future_number_field"] = [NSData dataWithBytes:&n length:sizeof(n)];
825 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
826 XCTAssertNil(error, "no error encrypting object");
827 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
829 CKRecord* ckr = [cipheritem CKRecordWithZoneID: recordID.zoneID];
830 ckr[@"future_data_field"] = future_data_field;
831 ckr[@"future_string_field"] = future_string_field;
832 ckr[@"future_number_field"] = future_number_field;
833 ckr[@"server_new_server_field"] = future_server_field;
834 [self.keychainZone addToZone:ckr];
836 [self.keychainView waitForFetchAndIncomingQueueProcessing];
838 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
839 (id)kSecReturnAttributes: @YES,
840 (id)kSecAttrSynchronizable: @YES,
841 (id)kSecAttrAccount: @"account-delete-me",
842 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
844 CFTypeRef cfresult = NULL;
845 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
847 // Test that if this item is updated, it remains encrypted in v2, and future_field still exists
848 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
849 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
851 OCMVerifyAllWithDelay(self.mockDatabase, 8);
852 [self waitForCKModifications];
854 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
855 XCTAssertEqualObjects(newRecord[@"future_data_field"], future_data_field, "future_data_field still exists");
856 XCTAssertEqualObjects(newRecord[@"future_string_field"], future_string_field, "future_string_field still exists");
857 XCTAssertEqualObjects(newRecord[@"future_number_field"], future_number_field, "future_string_field still exists");
858 XCTAssertEqualObjects(newRecord[@"server_new_server_field"], future_server_field, "future_server_field stille exists");
860 CKKSItem* newItem = [[CKKSItem alloc] initWithCKRecord:newRecord];
861 CKKSAESSIVKey* newItemKey = [self.keychainZoneKeys.classA unwrapAESKey:newItem.wrappedkey error:&error];
862 XCTAssertNil(error, "No error unwrapping AES key");
863 XCTAssertNotNil(newItemKey, "Have an unwrapped AES key for this item");
865 NSDictionary* uploadedData = [CKKSItemEncrypter decryptDictionary:newRecord[SecCKRecordDataKey]
867 authenticatedData:authenticatedData
869 XCTAssertNil(error, "No error decrypting dictionary");
870 XCTAssertNotNil(uploadedData, "Authenticated re-uploaded data including future_field");
871 XCTAssertEqualObjects(uploadedData[@"v_Data"], [@"different password" dataUsingEncoding:NSUTF8StringEncoding], "Passwords match");
875 -(void)testReceiveRecordEncryptedv1 {
876 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
878 [self startCKKSSubsystem];
879 [self.keychainView waitForKeyHierarchyReadiness];
881 NSError* error = nil;
883 // Manually encrypt an item
884 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
885 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
886 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
887 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
888 parentKeyUUID:self.keychainZoneKeys.classC.uuid
889 zoneID:recordID.zoneID];
890 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
891 XCTAssertNotNil(itemkey, "Got a key");
892 cipheritem.wrappedkey = itemkey.wrappedkey;
893 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
895 cipheritem.encver = CKKSItemEncryptionVersion1;
897 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:cipheritem.encver] mutableCopy];
899 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
900 XCTAssertNil(error, "no error encrypting object");
901 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
903 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
905 [self.keychainView waitForFetchAndIncomingQueueProcessing];
907 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
908 (id)kSecReturnAttributes: @YES,
909 (id)kSecAttrSynchronizable: @YES,
910 (id)kSecAttrAccount: @"account-delete-me",
911 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
913 CFTypeRef cfresult = NULL;
914 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
915 CFReleaseNull(cfresult);
917 // Test that if this item is updated, it is encrypted in v2
918 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
919 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
921 OCMVerifyAllWithDelay(self.mockDatabase, 4);
922 [self waitForCKModifications];
924 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
925 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
928 - (void)testUploadPagination {
929 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
931 for(size_t count = 0; count < 250; count++) {
932 [self addGenericPassword: @"data" account: [NSString stringWithFormat:@"account-delete-me-%03lu", count]];
935 [self startCKKSSubsystem];
937 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
938 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
939 [self expectCKModifyItemRecords: 50 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
941 OCMVerifyAllWithDelay(self.mockDatabase, 160);
944 - (void)testUploadInitialKeyHierarchy {
945 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
946 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
948 // Spin up CKKS subsystem.
949 [self startCKKSSubsystem];
951 OCMVerifyAllWithDelay(self.mockDatabase, 8);
954 - (void)testUploadInitialKeyHierarchyAfterLockedStart {
956 self.aksLockState = true;
957 [self.lockStateTracker recheck];
959 [self startCKKSSubsystem];
961 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
962 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:8*NSEC_PER_SEC], @"Key state should get stuck in waitforunlock");
964 // After unlock, the key hierarchy should be created.
965 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
967 self.aksLockState = false;
968 [self.lockStateTracker recheck];
970 OCMVerifyAllWithDelay(self.mockDatabase, 8);
972 // We expect a single class C record to be uploaded.
973 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
975 [self addGenericPassword: @"data" account: @"account-delete-me"];
976 OCMVerifyAllWithDelay(self.mockDatabase, 8);
979 - (void)testLockImmediatelyAfterUploadingInitialKeyHierarchy {
981 // Upon upload, block fetches
982 __weak __typeof(self) weakSelf = self;
983 [self expectCKModifyRecords: @{
984 SecCKRecordIntermediateKeyType: [NSNumber numberWithUnsignedInteger: 3],
985 SecCKRecordCurrentKeyType: [NSNumber numberWithUnsignedInteger: 3],
986 SecCKRecordTLKShareType: [NSNumber numberWithUnsignedInteger: 1],
988 deletedRecordTypeCounts:nil
989 zoneID:self.keychainZoneID
990 checkModifiedRecord:nil
991 runAfterModification:^{
992 __strong __typeof(self) strongSelf = weakSelf;
993 [strongSelf holdCloudKitFetches];
996 [self startCKKSSubsystem];
998 // Should enter 'ready'
999 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
1000 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1002 // Now, lock and allow fetches again
1003 self.aksLockState = true;
1004 [self.lockStateTracker recheck];
1005 [self releaseCloudKitFetchHold];
1007 CKKSResultOperation* op = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
1008 [op waitUntilFinished];
1010 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1012 // Wait for CKKS to shake itself out...
1013 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
1015 // Should be in ReadyPendingUnlock
1016 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:8*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1018 // We expect a single class C record to be uploaded.
1019 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1020 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1022 [self addGenericPassword: @"data" account: @"account-delete-me"];
1023 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1026 - (void)testReceiveKeyHierarchyAfterLockedStart {
1027 // 'Lock' the keybag
1028 self.aksLockState = true;
1029 [self.lockStateTracker recheck];
1031 [self startCKKSSubsystem];
1033 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1034 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateFetchComplete] wait:8*NSEC_PER_SEC], @"Key state should get stuck in fetchcomplete");
1036 // Now, another device comes along and creates the hierarchy; we download it; and it and sends us the TLK
1037 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1038 [self.keychainView notifyZoneChange:nil];
1039 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
1041 self.aksLockState = false;
1042 [self.lockStateTracker recheck];
1044 // After unlock, the TLK arrives
1045 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1046 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1048 // We expect a single class C record to be uploaded.
1049 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1051 [self addGenericPassword: @"data" account: @"account-delete-me"];
1052 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1055 - (void)testLoadKeyHierarchyAfterLockedStart {
1056 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
1058 // 'Lock' the keybag
1059 self.aksLockState = true;
1060 [self.lockStateTracker recheck];
1062 [self startCKKSSubsystem];
1064 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1065 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:8*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1067 self.aksLockState = false;
1068 [self.lockStateTracker recheck];
1070 // We expect a single class C record to be uploaded.
1071 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1073 [self addGenericPassword: @"data" account: @"account-delete-me"];
1074 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1077 - (void)testUploadAndUseKeyHierarchy {
1078 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
1079 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1081 [self startCKKSSubsystem];
1083 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
1084 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1085 (id)kSecAttrAccount : @"account-delete-me",
1086 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1087 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1089 CFTypeRef item = NULL;
1090 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not exist");
1092 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1093 [self waitForCKModifications];
1095 // We expect a single class C record to be uploaded.
1096 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1098 [self addGenericPassword: @"data" account: @"account-delete-me"];
1099 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1101 // now, expect a single class A record to be uploaded
1102 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1104 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef)@{
1105 (id)kSecClass : (id)kSecClassGenericPassword,
1106 (id)kSecAttrAccessGroup : @"com.apple.security.sos",
1107 (id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked,
1108 (id)kSecAttrAccount : @"account-class-A",
1109 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1110 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
1111 }, NULL), @"Adding class A item");
1112 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1115 - (void)testUploadInitialKeyHierarchyTriggersBackup {
1116 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
1117 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1119 // We also expect the view manager's notifyNewTLKsInKeychain call to fire (after some delay)
1120 OCMExpect([self.mockCKKSViewManager notifyNewTLKsInKeychain]);
1122 // Spin up CKKS subsystem.
1123 [self startCKKSSubsystem];
1125 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1126 OCMVerifyAllWithDelay(self.mockCKKSViewManager, 10);
1129 - (void)testResetCloudKitZoneFromNoTLK {
1130 self.silentZoneDeletesAllowed = true;
1132 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1133 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1134 // explicitly do not save a fake device status here
1135 self.keychainZone.flag = true;
1137 // It'll eventually upload a new key hierarchy
1138 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
1140 [self startCKKSSubsystem];
1141 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:8*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1143 // But then, it'll fire off the reset and reach 'ready'
1144 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1145 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
1147 // And the zone should have been cleared and re-made
1148 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1151 - (void)testResetCloudKitZoneFromNoTLKWithOtherWaitForTLKDevices {
1152 self.silentZoneDeletesAllowed = true;
1154 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1155 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1156 // Save a fake device status here, but modify its key state to be 'waitfortlk': it has no idea what the TLK is either
1157 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1159 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1160 if([record.recordType isEqualToString:SecCKRecordDeviceStateType]) {
1161 record[SecCKRecordKeyState] = CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTLK);
1165 self.keychainZone.flag = true;
1167 // It'll eventually upload a new key hierarchy
1168 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
1170 [self startCKKSSubsystem];
1171 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:8*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1173 // But then, it'll fire off the reset and reach 'ready'
1174 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1175 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
1177 // And the zone should have been cleared and re-made
1178 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1181 - (void)testResetCloudKitZoneFromNoTLKIgnoringInactiveDevices {
1182 self.silentZoneDeletesAllowed = true;
1184 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1185 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1186 // Save a fake device status here, but modify its creation and modification times to be months ago
1187 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1189 // Put a 'in-circle' TLKShare record, but also modify its creation and modification times
1190 CKKSSOSSelfPeer* untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"untrusted-peer"
1191 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1192 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
1193 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:untrustedPeer to:untrustedPeer zoneID:self.keychainZoneID];
1195 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1196 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1197 record.creationDate = [NSDate distantPast];
1198 record.modificationDate = [NSDate distantPast];
1202 self.keychainZone.flag = true;
1204 // It'll eventually upload a new key hierarchy
1205 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
1207 [self startCKKSSubsystem];
1208 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:8*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1210 // But then, it'll fire off the reset and reach 'ready'
1211 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1212 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
1214 // And the zone should have been cleared and re-made
1215 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1218 - (void)testDoNotResetCloudKitZoneFromWaitForTLKDueToRecentDeviceState {
1219 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1221 // CKKS shouldn't reset this zone, due to a recent device status claiming to have TLKs
1222 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1224 NSDateComponents* offset = [[NSDateComponents alloc] init];
1226 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1227 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1228 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1229 record.creationDate = updateTime;
1230 record.modificationDate = updateTime;
1234 self.keychainZone.flag = true;
1235 [self startCKKSSubsystem];
1237 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1239 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1242 - (void)testDoNotCloudKitZoneFromWaitForTLKDueToRecentButUntrustedDeviceState {
1243 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1245 // CKKS should reset this zone, even though to a recent device status claiming to have TLKs. The device isn't trusted
1246 self.silentZoneDeletesAllowed = true;
1247 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1248 [self.currentPeers removeObject:self.remoteSOSOnlyPeer];
1250 self.keychainZone.flag = true;
1251 [self startCKKSSubsystem];
1253 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1254 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1256 // And ensure it doesn't go on to 'reset'
1257 XCTAssertNotEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:100*NSEC_PER_MSEC], @"Key state should not become 'resetzone'");
1260 - (void)testResetCloudKitZoneFromWaitForTLKDueToLessRecentAndUntrustedDeviceState {
1261 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1263 // CKKS should reset this zone, even though to a recent device status claiming to have TLKs. The device isn't trusted
1264 self.silentZoneDeletesAllowed = true;
1265 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1266 [self.currentPeers removeObject:self.remoteSOSOnlyPeer];
1268 NSDateComponents* offset = [[NSDateComponents alloc] init];
1270 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1271 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1272 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1273 record.creationDate = updateTime;
1274 record.modificationDate = updateTime;
1278 self.keychainZone.flag = true;
1279 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
1280 [self startCKKSSubsystem];
1281 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:8*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1283 // Then we should reset.
1284 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1285 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
1287 // And the zone should have been cleared and re-made
1288 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1291 - (void)testAcceptExistingKeyHierarchy {
1292 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
1293 // Test also begins with the TLK having arrived in the local keychain (via SOS)
1294 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1295 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1296 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1298 // Spin up CKKS subsystem.
1299 [self startCKKSSubsystem];
1301 // The CKKS subsystem should only upload its TLK share
1302 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:5*NSEC_PER_SEC], "Key state should have become ready");
1304 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1306 // Verify that there are three local keys, and three local current key records
1307 __weak __typeof(self) weakSelf = self;
1308 [self.keychainView dispatchSync: ^bool{
1309 __strong __typeof(weakSelf) strongSelf = weakSelf;
1310 XCTAssertNotNil(strongSelf, "self exists");
1312 NSError* error = nil;
1314 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
1315 XCTAssertNil(error, "no error fetching keys");
1316 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
1318 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1319 XCTAssertNil(error, "no error fetching current keys");
1320 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1326 - (void)testAcceptExistingAndUseKeyHierarchy {
1327 // Test starts with nothing in database, but one in our fake CloudKit.
1328 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1329 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1330 // But, CKKS shouldn't ever reset the zone
1331 self.keychainZone.flag = true;
1333 [self startCKKSSubsystem];
1334 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:5*NSEC_PER_SEC], "Key state should have become waitfortlk");
1336 // Now, save the TLK to the keychain (to simulate it coming in later via SOS). We'll create a TLK share for ourselves.
1337 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1338 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1340 // Wait for the key hierarchy to sort itself out, to make it easier on this test; see testOnboardOldItemsWithExistingKeyHierarchy for the other test.
1341 // The CKKS subsystem should write its TLK share, but nothing else
1342 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:5*NSEC_PER_SEC], "Key state should have become ready");
1344 // We expect a single record to be uploaded for each key class
1345 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1346 [self addGenericPassword: @"data" account: @"account-delete-me"];
1347 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1349 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1350 [self addGenericPassword:@"asdf"
1351 account:@"account-class-A"
1353 access:(id)kSecAttrAccessibleWhenUnlocked
1354 expecting:errSecSuccess
1355 message:@"Adding class A item"];
1356 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1357 XCTAssertTrue(self.keychainZone.flag, "Keychain zone shouldn't have been reset");
1360 - (void)testAcceptExistingKeyHierarchyDespiteLocked {
1361 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
1362 // Test also begins with the TLK having arrived in the local keychain (via SOS)
1363 // However, the CKKSKeychainView's "_onqueueWithAccountKeysCheckTLK" method should return a keychain error the first time through, indicating that the keybag is locked
1364 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1365 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1367 self.aksLockState = true;
1368 [self.lockStateTracker recheck];
1370 id partialKVMock = OCMPartialMock(self.keychainView);
1371 OCMExpect([partialKVMock _onqueueWithAccountKeysCheckTLK: [OCMArg any] error: [OCMArg setTo:[[NSError alloc] initWithDomain:@"securityd" code:errSecInteractionNotAllowed userInfo:nil]]]).andReturn(false);
1373 // Spin up CKKS subsystem.
1374 [self startCKKSSubsystem];
1376 OCMVerifyAllWithDelay(partialKVMock, 4);
1378 // CKKS will give itself a TLK Share
1379 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1381 // Now that all operations are complete, 'unlock' AKS
1382 self.aksLockState = false;
1383 [self.lockStateTracker recheck];
1385 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:5*NSEC_PER_SEC], "Key state should have become ready");
1386 OCMVerifyAllWithDelay(self.mockDatabase, 4);
1388 // Verify that there are three local keys, and three local current key records
1389 __weak __typeof(self) weakSelf = self;
1390 [self.keychainView dispatchSync: ^bool{
1391 __strong __typeof(weakSelf) strongSelf = weakSelf;
1392 XCTAssertNotNil(strongSelf, "self exists");
1394 NSError* error = nil;
1396 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
1397 XCTAssertNil(error, "no error fetching keys");
1398 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
1400 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1401 XCTAssertNil(error, "no error fetching current keys");
1402 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1407 [partialKVMock stopMocking];
1410 - (void)testReceiveClassCWhileALocked {
1411 // Test starts with a key hierarchy already existing.
1412 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
1413 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1414 [self startCKKSSubsystem];
1416 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
1417 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1419 [self findGenericPassword:@"classCItem" expecting:errSecItemNotFound];
1420 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
1422 // 'Lock' the keybag
1423 self.aksLockState = true;
1424 [self.lockStateTracker recheck];
1426 XCTAssertNotNil(self.keychainZoneKeys, "Have zone keys for zone");
1427 XCTAssertNotNil(self.keychainZoneKeys.classA, "Have class A key for zone");
1428 XCTAssertNotNil(self.keychainZoneKeys.classC, "Have class C key for zone");
1430 [self.keychainView dispatchSyncWithAccountKeys: ^bool {
1431 [self.keychainView _onqueueKeyStateMachineRequestProcess];
1434 // And ensure we end up back in 'readypendingunlock': we have the keys, we're just locked now
1435 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
1436 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:8*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1438 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"classCItem" key:self.keychainZoneKeys.classC]];
1439 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-FFFF-FFFF-FFFF-5A507ACB2D85" withAccount:@"classAItem" key:self.keychainZoneKeys.classA]];
1441 CKKSResultOperation* op = [self.keychainView waitForFetchAndIncomingQueueProcessing];
1442 // The processing op should NOT error, even though it didn't manage to process the classA item
1443 XCTAssertNil(op.error, "no error while failing to process a class A item");
1445 CKKSResultOperation* erroringOp = [self.keychainView processIncomingQueue:true];
1446 [erroringOp waitUntilFinished];
1447 XCTAssertNotNil(erroringOp.error, "error exists while processing a class A item");
1449 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
1450 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
1452 self.aksLockState = false;
1453 [self.lockStateTracker recheck];
1454 [self.keychainView waitUntilAllOperationsAreFinished];
1456 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
1457 [self findGenericPassword:@"classAItem" expecting:errSecSuccess];
1460 - (void)testRestartWhileLocked {
1461 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1462 [self startCKKSSubsystem];
1464 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
1466 // 'Lock' the keybag
1467 self.aksLockState = true;
1468 [self.lockStateTracker recheck];
1470 [self.keychainView halt];
1471 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1473 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:8*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1475 self.aksLockState = false;
1476 [self.lockStateTracker recheck];
1478 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
1481 - (void)testExternalKeyRoll {
1482 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1483 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1484 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1485 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1487 // Spin up CKKS subsystem.
1488 [self startCKKSSubsystem];
1490 // The CKKS subsystem should not try to write anything to the CloudKit database.
1491 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1493 __weak __typeof(self) weakSelf = self;
1495 // We expect a single record to be uploaded.
1496 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1498 [self addGenericPassword: @"data" account: @"account-delete-me"];
1500 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1501 [self waitForCKModifications];
1503 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1504 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1505 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1507 // Trigger a notification
1508 [self.keychainView notifyZoneChange:nil];
1510 // Make life easy on this test; testAcceptKeyConflictAndUploadReencryptedItem will check the case when we don't receive the notification
1511 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1513 // Just in extra case of threading issues, force a reexamination of the key hierarchy
1514 [self.keychainView dispatchSyncWithAccountKeys: ^bool {
1515 [self.keychainView _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
1519 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
1521 // Verify that there are six local keys, and three local current key records
1522 [self.keychainView dispatchSync: ^bool{
1523 __strong __typeof(weakSelf) strongSelf = weakSelf;
1524 XCTAssertNotNil(strongSelf, "self exists");
1526 NSError* error = nil;
1527 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:self.keychainZoneID error:&error];
1528 XCTAssertNil(error, "no error fetching keys");
1529 XCTAssertEqual(keys.count, 6u, "Six keys in local database");
1531 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1532 XCTAssertNil(error, "no error fetching current keys");
1533 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1535 for(CKKSCurrentKeyPointer* key in currentkeys) {
1536 if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
1537 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.tlk.uuid);
1538 } else if([key.keyclass isEqualToString: SecCKKSKeyClassA]) {
1539 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classA.uuid);
1540 } else if([key.keyclass isEqualToString: SecCKKSKeyClassC]) {
1541 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classC.uuid);
1543 XCTFail("Unknown key class: %@", key.keyclass);
1550 // We expect a single record to be uploaded.
1551 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1553 // TODO: remove this by writing code for item reencrypt after key arrival
1554 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1556 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1558 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1561 - (void)testAcceptKeyConflictAndUploadReencryptedItem {
1562 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1563 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1564 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1565 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1567 [self startCKKSSubsystem];
1568 [self.keychainView waitUntilAllOperationsAreFinished];
1570 // We expect a single record to be uploaded.
1571 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1573 [self addGenericPassword: @"data" account: @"account-delete-me"];
1575 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1576 [self waitForCKModifications];
1578 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1580 // Do not trigger a notification here. This should cause a conflict updating the current key records
1582 // We expect a single record to be uploaded, but that the write will be rejected
1583 // We then expect that item to be reuploaded with the current key
1585 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
1586 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1587 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1589 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under rolled class C key in hierarchy"]];
1591 // New key arrives via SOS!
1592 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1593 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1595 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1598 - (void)testAcceptKeyConflictAndUploadReencryptedItems {
1599 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1600 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1601 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1602 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1604 [self startCKKSSubsystem];
1605 [self.keychainView waitUntilAllOperationsAreFinished];
1607 // We expect a single record to be uploaded.
1608 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1609 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1611 [self addGenericPassword: @"data" account: @"account-delete-me"];
1613 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1614 [self waitForCKModifications];
1616 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1618 // Do not trigger a notification here. This should cause a conflict updating the current key records
1620 // We expect a single record to be uploaded, but that the write will be rejected
1621 // We then expect that item to be reuploaded with the current key
1623 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
1624 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1625 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key-2"];
1626 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1628 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1629 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under rolled class C key in hierarchy"]];
1631 // New key arrives via SOS!
1632 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1633 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1635 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1638 - (void)testRecoverFromRequestKeyRefetchWithoutRolling {
1639 // Simply requesting a key state refetch shouldn't roll the key hierarchy.
1641 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1643 // Spin up CKKS subsystem.
1644 [self startCKKSSubsystem];
1646 // Items should upload.
1647 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1648 [self addGenericPassword: @"data" account: @"account-delete-me"];
1649 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1651 [self waitForCKModifications];
1654 // CKKS should not roll the keys while progressing back to 'ready', but it will fetch once
1655 self.silentFetchesAllowed = false;
1656 [self expectCKFetch];
1658 [self.keychainView dispatchSyncWithAccountKeys: ^bool {
1659 [self.keychainView _onqueueKeyStateMachineRequestFetch];
1663 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:5*NSEC_PER_SEC], "Key state should have returned to ready");
1664 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1667 - (void)testRecoverFromIncrementedCurrentKeyPointerEtag {
1668 // CloudKit sometimes reports the current key pointers have changed (etag mismatch), but their content hasn't.
1669 // In this case, CKKS shouldn't roll the TLK.
1671 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1673 // Spin up CKKS subsystem.
1674 [self startCKKSSubsystem];
1675 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // just to be sure it's fetched
1677 // Items should upload.
1678 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1679 [self addGenericPassword: @"data" account: @"account-delete-me"];
1680 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1682 [self waitForCKModifications];
1684 // Bump the etag on the class C current key record, but don't change any data
1685 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
1686 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
1687 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
1689 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
1690 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
1692 // Add another item. This write should fail, then CKKS should recover without rolling the key hierarchy or issuing a fetch.
1693 self.silentFetchesAllowed = false;
1694 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1695 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1696 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
1697 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1700 - (void)testRecoverMultipleItemsFromIncrementedCurrentKeyPointerEtag {
1701 // CloudKit sometimes reports the current key pointers have changed (etag mismatch), but their content hasn't.
1702 // In this case, CKKS shouldn't roll the TLK.
1703 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1705 // Spin up CKKS subsystem.
1706 [self startCKKSSubsystem];
1707 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // just to be sure it's fetched
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, 8);
1714 [self waitForCKModifications];
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");
1721 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
1722 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
1724 // Add another item. This write should fail, then CKKS should recover without rolling the key hierarchy or issuing a fetch.
1725 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
1726 secnotice("ckks", "releasing outgoing-queue hold");
1729 self.silentFetchesAllowed = false;
1730 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1731 [self expectCKModifyItemRecords:2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1732 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
1733 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
1735 [self.operationQueue addOperation: self.keychainView.holdOutgoingQueueOperation];
1736 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1739 - (void)testOnboardOldItemsCreatingKeyHierarchy {
1740 // 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
1742 // Test starts with nothing in CloudKit, and CKKS blocked. Add one item without a UUID...
1744 SecCKKSTestSetDisableAutomaticUUID(true);
1745 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1747 // and an item with a UUID...
1748 SecCKKSTestSetDisableAutomaticUUID(false);
1749 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1751 // We expect an upload of the key hierarchy
1752 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1754 // We then expect an upload of the added items
1755 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1757 [self startCKKSSubsystem];
1759 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1762 - (void)testOnboardOldItemsWithExistingKeyHierarchy {
1763 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1765 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1766 [self addGenericPassword: @"data" account: @"account-delete-me"];
1768 [self startCKKSSubsystem];
1769 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1772 - (void)testOnboardOldItemsWithExistingKeyHierarchyExtantTLK {
1773 // Test starts key hierarchy in our fake CloudKit, the TLK arrived in the local keychain, and CKKS blocked.
1774 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1775 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1776 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1778 // Add one item without a UUID...
1779 SecCKKSTestSetDisableAutomaticUUID(true);
1780 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1782 // and an item with a UUID...
1783 SecCKKSTestSetDisableAutomaticUUID(false);
1784 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1786 // 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
1787 // We expect a single record to be uploaded.
1788 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1790 // Spin up CKKS subsystem.
1791 [self startCKKSSubsystem];
1793 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1796 - (void)testOnboardOldItemsWithExistingKeyHierarchyLateTLK {
1797 // Test starts key hierarchy in our fake CloudKit, and CKKS blocked.
1798 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1799 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1800 self.keychainZone.flag = true;
1802 // Add one item without a UUID...
1803 SecCKKSTestSetDisableAutomaticUUID(true);
1804 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1806 // and an item with a UUID...
1807 SecCKKSTestSetDisableAutomaticUUID(false);
1808 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1810 // 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
1812 // Spin up CKKS subsystem.
1813 [self startCKKSSubsystem];
1814 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:5*NSEC_PER_SEC], "Key state should have become waitfortlk");
1816 // Now, save the TLK to the keychain (to simulate it coming in via SOS).
1817 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1818 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1820 // We expect a single record to be uploaded.
1821 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1823 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1824 XCTAssertTrue(self.keychainZone.flag, "Keychain zone shouldn't have been reset");
1827 - (void)testResync {
1828 // We need to set up a desynced situation to test our resync.
1829 // First, let CKKS start up and send several items to CloudKit (that we'll then desync!)
1830 __block NSError* error = nil;
1832 // Test starts with keys in CloudKit (so we can create items later)
1833 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1834 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1835 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1837 [self addGenericPassword: @"data" account: @"first"];
1838 [self addGenericPassword: @"data" account: @"second"];
1839 [self addGenericPassword: @"data" account: @"third"];
1840 [self addGenericPassword: @"data" account: @"fourth"];
1841 NSUInteger passwordCount = 4u;
1843 [self checkGenericPassword: @"data" account: @"first"];
1844 [self checkGenericPassword: @"data" account: @"second"];
1845 [self checkGenericPassword: @"data" account: @"third"];
1846 [self checkGenericPassword: @"data" account: @"fourth"];
1848 [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1850 [self startCKKSSubsystem];
1852 // Wait for uploads to happen
1853 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1854 [self waitForCKModifications];
1855 // One TLK share record
1856 XCTAssertEqual(self.keychainZone.currentDatabase.count, SYSTEM_DB_RECORD_COUNT+passwordCount+1, "Have SYSTEM_DB_RECORD_COUNT+passwordCount+1 objects in cloudkit");
1858 // Now, corrupt away!
1859 // Extract all passwordCount items for Corruption
1860 NSArray<CKRecord*>* items = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
1861 XCTAssertEqual(items.count, passwordCount, "Have %lu Items in cloudkit", (unsigned long)passwordCount);
1863 // For the first record, delete all traces of it from CKKS. But! it remains in local keychain.
1864 // Expected outcome: CKKS resyncs; item exists again.
1865 CKRecord* delete = items[0];
1866 NSString* deleteAccount = [[self decryptRecord: delete] objectForKey: (__bridge id) kSecAttrAccount];
1867 XCTAssertNotNil(deleteAccount, "received an account for the local delete object");
1869 __weak __typeof(self) weakSelf = self;
1870 [self.keychainView dispatchSync:^bool{
1871 __strong __typeof(weakSelf) strongSelf = weakSelf;
1872 XCTAssertNotNil(strongSelf, "self exists");
1874 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1876 [ckme deleteFromDatabase: &error];
1878 XCTAssertNil(error, "no error removing CKME");
1879 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1881 [oqe deleteFromDatabase: &error];
1883 XCTAssertNil(error, "no error removing OQE");
1884 CKKSIncomingQueueEntry* iqe = [CKKSIncomingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1886 [iqe deleteFromDatabase: &error];
1888 XCTAssertNil(error, "no error removing IQE");
1892 // For the second record, delete all traces of it from CloudKit.
1893 // Expected outcome: deleted locally
1894 CKRecord* remoteDelete = items[1];
1895 NSString* remoteDeleteAccount = [[self decryptRecord: remoteDelete] objectForKey: (__bridge id) kSecAttrAccount];
1896 XCTAssertNotNil(remoteDeleteAccount, "received an account for the remote delete object");
1898 [self.keychainZone deleteCKRecordIDFromZone: remoteDelete.recordID];
1899 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
1900 [database removeObjectForKey: remoteDelete.recordID];
1903 // The third record gets modified in CloudKit, but not locally.
1904 // Expected outcome: use the CloudKit version
1905 CKRecord* remoteDataChanged = items[2];
1906 NSMutableDictionary* remoteDataDictionary = [[self decryptRecord: remoteDataChanged] mutableCopy];
1907 NSString* remoteDataChangedAccount = [remoteDataDictionary objectForKey: (__bridge id) kSecAttrAccount];
1908 XCTAssertNotNil(remoteDataChangedAccount, "Received an account for the remote-data-changed object");
1909 remoteDataDictionary[(__bridge id) kSecValueData] = [@"CloudKitWins" dataUsingEncoding: NSUTF8StringEncoding];
1911 CKRecord* newData = [self newRecord: remoteDataChanged.recordID withNewItemData: remoteDataDictionary];
1912 [self.keychainZone addToZone: newData];
1913 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
1914 database[remoteDataChanged.recordID] = newData;
1917 // The fourth record stays in-sync. Good work, everyone!
1918 // Expected outcome: stays in-sync
1919 NSString* insyncAccount = [[self decryptRecord: items[3]] objectForKey: (__bridge id) kSecAttrAccount];
1920 XCTAssertNotNil(insyncAccount, "Received an account for the in-sync object");
1922 // The fifth record gets magically added to CloudKit, but CKKS has never heard of it
1923 // (emulates a lost record on the client, but that CloudKit already believes it's sent the record for)
1924 // Expected outcome: added to local keychain
1925 NSString* remoteOnlyAccount = @"remote-only";
1926 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount: remoteOnlyAccount];
1927 [self.keychainZone addToZone: ckr];
1928 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
1929 database[ckr.recordID] = ckr;
1932 ckksnotice("ckksresync", self.keychainView, "local delete: %@ %@", delete.recordID.recordName, deleteAccount);
1933 ckksnotice("ckksresync", self.keychainView, "Remote deletion: %@ %@", remoteDelete.recordID.recordName, remoteDeleteAccount);
1934 ckksnotice("ckksresync", self.keychainView, "Remote data changed: %@ %@", remoteDataChanged.recordID.recordName, remoteDataChangedAccount);
1935 ckksnotice("ckksresync", self.keychainView, "in-sync: %@ %@", items[3].recordID.recordName, insyncAccount);
1936 ckksnotice("ckksresync", self.keychainView, "Remote only: %@ %@", ckr.recordID.recordName, remoteOnlyAccount);
1938 CKKSSynchronizeOperation* resyncOperation = [self.keychainView resyncWithCloud];
1939 [resyncOperation waitUntilFinished];
1941 XCTAssertNil(resyncOperation.error, "No error during the resync operation");
1943 // Now do some checking. Remember, we don't know which record we corrupted, so use the parsed account variables to check.
1945 [self findGenericPassword: deleteAccount expecting: errSecSuccess];
1946 [self findGenericPassword: remoteDeleteAccount expecting: errSecItemNotFound];
1947 [self findGenericPassword: remoteDataChangedAccount expecting: errSecSuccess];
1948 [self findGenericPassword: insyncAccount expecting: errSecSuccess];
1949 [self findGenericPassword: remoteOnlyAccount expecting: errSecSuccess];
1951 [self checkGenericPassword: @"data" account: deleteAccount];
1952 //[self checkGenericPassword: @"data" account: remoteDeleteAccount];
1953 [self checkGenericPassword: @"CloudKitWins" account: remoteDataChangedAccount];
1954 [self checkGenericPassword: @"data" account: insyncAccount];
1955 [self checkGenericPassword: @"data" account: remoteOnlyAccount];
1957 [self.keychainView dispatchSync:^bool{
1958 __strong __typeof(weakSelf) strongSelf = weakSelf;
1959 XCTAssertNotNil(strongSelf, "self exists");
1961 CKKSMirrorEntry* ckme = nil;
1963 ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1964 XCTAssertNil(error);
1965 XCTAssertNotNil(ckme);
1967 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDelete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1968 XCTAssertNil(error);
1969 XCTAssertNil(ckme); // deleted!
1971 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDataChanged.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1972 XCTAssertNil(error);
1973 XCTAssertNotNil(ckme);
1975 ckme = [CKKSMirrorEntry tryFromDatabase:items[3].recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1976 XCTAssertNil(error);
1977 XCTAssertNotNil(ckme);
1979 ckme = [CKKSMirrorEntry tryFromDatabase:ckr.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1980 XCTAssertNil(error);
1981 XCTAssertNotNil(ckme);
1986 - (void)testResyncItemsMissingFromLocalKeychain {
1987 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1990 // one password correctly synced between local keychain and CloudKit
1991 // one password incorrectly disappeared from local keychain, but in mirror table
1992 // one password sitting in the outgoing queue
1993 // one password sitting in the incoming queue
1995 // Add and sync two passwords
1996 [self addGenericPassword: @"data" account: @"first"];
1997 [self addGenericPassword: @"data" account: @"second"];
1999 [self checkGenericPassword: @"data" account: @"first"];
2000 [self checkGenericPassword: @"data" account: @"second"];
2002 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2003 [self startCKKSSubsystem];
2004 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2005 [self waitForCKModifications];
2006 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2008 // Now, place an item in the outgoing queue
2010 //[self addGenericPassword: @"data" account: @"third"];
2011 //[self checkGenericPassword: @"data" account: @"third"];
2013 // Now, corrupt away!
2014 // Extract all passwordCount items for Corruption
2015 NSArray<CKRecord*>* items = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
2016 XCTAssertEqual(items.count, 2u, "Have %lu Items in cloudkit", (unsigned long)2u);
2018 // For the first record, surreptitiously remove from local keychain
2019 CKRecord* remove = items[0];
2020 NSString* removeAccount = [[self decryptRecord:remove] objectForKey:(__bridge id)kSecAttrAccount];
2021 XCTAssertNotNil(removeAccount, "received an account for the local delete object");
2023 NSURL* kcpath = (__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)@"keychain-2-debug.db");
2025 sqlite3_open([[kcpath path] UTF8String], &db);
2026 NSString* query = [NSString stringWithFormat:@"DELETE FROM genp WHERE uuid=\"%@\"", remove.recordID.recordName];
2027 char* sqlerror = NULL;
2028 XCTAssertEqual(SQLITE_OK, sqlite3_exec(db, [query UTF8String], NULL, NULL, &sqlerror), "SQL deletion shouldn't error");
2029 XCTAssertTrue(sqlerror == NULL, "No error string should have been returned: %s", sqlerror);
2031 sqlite3_free(sqlerror);
2036 // The second record is kept in-sync
2038 // Now, add an in-flight change (for record 3)
2039 [self holdCloudKitModifications];
2040 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2041 [self addGenericPassword:@"data" account:@"third"];
2042 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2044 // For the fourth, add a new record but prevent incoming queue processing
2045 self.keychainView.holdIncomingQueueOperation = [CKKSResultOperation named:@"hold-incoming" withBlock:^{}];
2047 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"fourth"];
2048 [self.keychainZone addToZone:ckr];
2049 [self.keychainView notifyZoneChange:nil];
2051 // Now, where are we....
2052 CKKSScanLocalItemsOperation* scanLocal = [self.keychainView scanLocalItems:@"test-scan"];
2053 [scanLocal waitUntilFinished];
2055 XCTAssertEqual(scanLocal.missingLocalItemsFound, 1u, "Should have found one missing item");
2057 // Allow everything to proceed
2058 [self releaseCloudKitModificationHold];
2059 [self.operationQueue addOperation:self.keychainView.holdIncomingQueueOperation];
2061 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2062 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2064 // And ensure that all four items are present again
2065 [self findGenericPassword: @"first" expecting: errSecSuccess];
2066 [self findGenericPassword: @"second" expecting: errSecSuccess];
2067 [self findGenericPassword: @"third" expecting: errSecSuccess];
2068 [self findGenericPassword: @"fourth" expecting: errSecSuccess];
2071 - (void)testResyncLocal {
2072 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2073 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2074 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2076 [self addGenericPassword: @"data" account: @"first"];
2077 [self addGenericPassword: @"data" account: @"second"];
2078 NSUInteger passwordCount = 2u;
2080 [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2081 [self startCKKSSubsystem];
2083 // Wait for uploads to happen
2084 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2085 [self waitForCKModifications];
2087 // Local resyncs shouldn't fetch clouds.
2088 self.silentFetchesAllowed = false;
2090 [self deleteGenericPassword:@"first"];
2091 [self deleteGenericPassword:@"second"];
2094 // And they're gone!
2095 [self findGenericPassword:@"first" expecting:errSecItemNotFound];
2096 [self findGenericPassword:@"second" expecting:errSecItemNotFound];
2098 CKKSLocalSynchronizeOperation* op = [self.keychainView resyncLocal];
2099 [op waitUntilFinished];
2100 XCTAssertNil(op.error, "Shouldn't be an error resyncing locally");
2102 // And they're back!
2103 [self checkGenericPassword: @"data" account: @"first"];
2104 [self checkGenericPassword: @"data" account: @"second"];
2107 - (void)testPlistRestoreResyncsLocal {
2108 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2109 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2110 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2112 [self addGenericPassword: @"data" account: @"first"];
2113 [self addGenericPassword: @"data" account: @"second"];
2114 NSUInteger passwordCount = 2u;
2116 [self checkGenericPassword: @"data" account: @"first"];
2117 [self checkGenericPassword: @"data" account: @"second"];
2119 [self expectCKModifyItemRecords:passwordCount currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2120 [self startCKKSSubsystem];
2122 // Wait for uploads to happen
2123 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2124 [self waitForCKModifications];
2127 // This 'restores' a plist keychain backup
2128 // That will kick off a local resync in CKKS, so hold that until we're ready...
2129 self.keychainView.holdLocalSynchronizeOperation = [CKKSResultOperation named:@"hold-local-synchronize" withBlock:^{}];
2131 // Local resyncs shouldn't fetch clouds.
2132 self.silentFetchesAllowed = false;
2134 CFErrorRef cferror = NULL;
2135 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
2136 CFErrorRef cfcferror = NULL;
2138 bool ret = SecServerImportKeychainInPlist(dbt, SecSecurityClientGet(), KEYBAG_NONE, KEYBAG_NONE,
2139 (__bridge CFDictionaryRef)@{}, kSecBackupableItemFilter, false, &cfcferror);
2141 XCTAssertNil(CFBridgingRelease(cfcferror), "Shouldn't error importing a 'backup'");
2142 XCTAssert(ret, "Importing a 'backup' should have succeeded");
2145 XCTAssertNil(CFBridgingRelease(cferror), "Shouldn't error mucking about in the db");
2147 // Restore is additive so original items stick around
2148 [self findGenericPassword:@"first" expecting:errSecSuccess];
2149 [self findGenericPassword:@"second" expecting:errSecSuccess];
2151 // Allow the local resync to continue...
2152 [self.operationQueue addOperation:self.keychainView.holdLocalSynchronizeOperation];
2153 [self.keychainView waitForOperationsOfClass:[CKKSLocalSynchronizeOperation class]];
2155 // Items are still here!
2156 [self checkGenericPassword: @"data" account: @"first"];
2157 [self checkGenericPassword: @"data" account: @"second"];
2160 - (void)testMultipleZoneAdd {
2161 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2162 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2164 // Bring up a new zone: we expect a key hierarchy upload.
2165 [self.injectedManager findOrCreateView:(id)kSecAttrViewHintAppleTV];
2166 CKRecordZoneID* appleTVZoneID = [[CKRecordZoneID alloc] initWithZoneName:(__bridge NSString*) kSecAttrViewHintAppleTV ownerName:CKCurrentUserDefaultName];
2167 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:appleTVZoneID];
2169 // We also expect the view manager's notifyNewTLKsInKeychain call to fire once (after some delay)
2170 OCMExpect([self.mockCKKSViewManager notifyNewTLKsInKeychain]);
2172 // Let the horses loose
2173 [self startCKKSSubsystem];
2175 // We expect a single record to be uploaded to the 'keychain' view
2176 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2177 [self addGenericPassword: @"data" account: @"account-delete-me"];
2178 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2180 // We expect a single record to be uploaded to the 'atv' view
2181 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:appleTVZoneID];
2182 [self addGenericPassword: @"atv"
2183 account: @"tvaccount"
2184 viewHint:(__bridge NSString*) kSecAttrViewHintAppleTV
2185 access:(id)kSecAttrAccessibleAfterFirstUnlock
2186 expecting:errSecSuccess message:@"AppleTV view-hinted object"];
2188 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2190 OCMVerifyAllWithDelay(self.mockCKKSViewManager, 10);
2193 - (void)testMultipleZoneDelete {
2194 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2195 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2197 [self startCKKSSubsystem];
2199 // We expect a single record to be uploaded.
2200 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2201 [self addGenericPassword: @"data" account: @"account-delete-me"];
2202 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2204 // Bring up a new zone: we expect a key hierarchy and an item.
2205 [self.injectedManager findOrCreateView:(id)kSecAttrViewHintAppleTV];
2206 CKRecordZoneID* appleTVZoneID = [[CKRecordZoneID alloc] initWithZoneName:(__bridge NSString*) kSecAttrViewHintAppleTV ownerName:CKCurrentUserDefaultName];
2207 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:appleTVZoneID];
2208 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:appleTVZoneID];
2210 [self addGenericPassword: @"atv"
2211 account: @"tvaccount"
2212 viewHint:(__bridge NSString*) kSecAttrViewHintAppleTV
2213 access:(id)kSecAttrAccessibleAfterFirstUnlock
2214 expecting:errSecSuccess
2215 message:@"AppleTV view-hinted object"];
2216 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2218 // We expect a single record to be deleted from the ATV zone
2219 [self expectCKDeleteItemRecords: 1 zoneID:appleTVZoneID];
2220 [self deleteGenericPassword:@"tvaccount"];
2221 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2223 // Now we expect a single record to be deleted from the test zone
2224 [self expectCKDeleteItemRecords: 1 zoneID:self.keychainZoneID];
2225 [self deleteGenericPassword:@"account-delete-me"];
2226 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2229 - (void)testRestartWithoutRefetch {
2230 // Restarting the CKKS operation should check that it's been 15 minutes since the last fetch before it fetches again. Simulate this.
2232 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2233 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2234 [self startCKKSSubsystem];
2236 [self.keychainView waitForKeyHierarchyReadiness];
2237 [self waitForCKModifications];
2238 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2240 // Tear down the CKKS object and disallow fetches
2241 [self.keychainView halt];
2242 self.silentFetchesAllowed = false;
2244 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2245 [self.keychainView waitForKeyHierarchyReadiness];
2246 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2248 // Okay, cool, rad, now let's set the date to be very long ago and check that there's positively a fetch
2249 [self.keychainView halt];
2250 self.silentFetchesAllowed = false;
2252 [self.keychainView dispatchSync: ^bool {
2253 NSError* error = nil;
2254 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
2256 XCTAssertNil(error, "no error pulling ckse from database");
2257 XCTAssertNotNil(ckse, "received a ckse");
2259 ckse.lastFetchTime = [NSDate distantPast];
2260 [ckse saveToDatabase: &error];
2261 XCTAssertNil(error, "no error saving to database");
2265 [self expectCKFetch];
2266 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2267 [self.keychainView waitForKeyHierarchyReadiness];
2268 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2271 - (void)testRecoverFromZoneCreationFailure {
2272 // Fail the zone creation.
2273 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
2274 [self failNextZoneCreation:self.keychainZoneID];
2276 // Spin up CKKS subsystem.
2277 [self startCKKSSubsystem];
2279 // The CKKS subsystem should figure out the issue, and fix it.
2280 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2282 [self.keychainView waitForKeyHierarchyReadiness];
2283 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2285 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2286 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2287 [self addGenericPassword: @"data" account: @"account-delete-me"];
2288 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2290 XCTAssertNil(self.zones[self.keychainZoneID].creationError, "Creation error was unset (and so CKKS probably dealt with the error");
2293 - (void)testRecoverFromZoneSubscriptionFailure {
2294 // Fail the zone subscription.
2295 [self failNextZoneSubscription:self.keychainZoneID];
2297 // Spin up CKKS subsystem.
2298 [self startCKKSSubsystem];
2300 // The CKKS subsystem should figure out the issue, and fix it.
2301 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2303 [self.keychainView waitForKeyHierarchyReadiness];
2304 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2306 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2307 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2308 [self addGenericPassword: @"data" account: @"account-delete-me"];
2309 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2311 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
2314 - (void)testRecoverFromZoneSubscriptionFailureDueToZoneNotExisting {
2315 // This is different from testRecoverFromZoneSubscriptionFailure, since the zone is gone. CKKS must attempt to re-create the zone.
2317 // Silently fail the zone creation
2318 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
2319 [self failNextZoneCreationSilently:self.keychainZoneID];
2321 // Spin up CKKS subsystem.
2322 [self startCKKSSubsystem];
2324 // The CKKS subsystem should figure out the issue, and fix it.
2325 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2327 [self.keychainView waitForKeyHierarchyReadiness];
2328 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2330 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2331 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2332 [self addGenericPassword: @"data" account: @"account-delete-me"];
2333 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2335 XCTAssertFalse(self.zones[self.keychainZoneID].flag, "Zone flag was reset");
2336 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
2339 - (void)testRecoverFromDeletedTLKWithStashedTLK {
2340 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
2342 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2343 NSError* error = nil;
2346 [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
2347 XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
2349 // And delete the non-stashed version
2350 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
2351 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
2353 // Spin up CKKS subsystem.
2354 [self startCKKSSubsystem];
2356 [self.keychainView waitForKeyHierarchyReadiness];
2357 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2359 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2360 [self addGenericPassword: @"data" account: @"account-delete-me"];
2361 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2363 // CKKS should recreate the syncable TLK.
2364 [self checkNSyncableTLKsInKeychain: 1];
2367 - (void)testRecoverFromDeletedTLKWithStashedTLKUponRestart {
2368 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
2370 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2371 // Spin up CKKS subsystem.
2372 [self startCKKSSubsystem];
2373 [self.keychainView waitForKeyHierarchyReadiness];
2375 // Tear down the CKKS object
2376 [self.keychainView halt];
2378 NSError* error = nil;
2381 [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
2382 XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
2384 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
2385 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
2387 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2388 [self.keychainView waitForKeyHierarchyReadiness];
2389 OCMVerifyAllWithDelay(self.mockDatabase, 8);
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, 8);
2395 // CKKS should recreate the syncable TLK.
2396 [self checkNSyncableTLKsInKeychain: 1];
2399 - (void)testRecoverFromTLKWriteFailure {
2400 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
2401 // Test starts with nothing in CloudKit, and will fail the first TLK write.
2402 NSError* noNetwork = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}];
2403 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:noNetwork];
2405 // Spin up CKKS subsystem.
2406 [self startCKKSSubsystem];
2408 // The CKKS subsystem should figure out the issue, and fix it.
2409 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2411 [self.keychainView waitForKeyHierarchyReadiness];
2412 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2414 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2415 [self addGenericPassword: @"data" account: @"account-delete-me"];
2416 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2418 // A network failure creating new TLKs shouldn't delete the 'failed' syncable one.
2419 [self checkNSyncableTLKsInKeychain: 2];
2422 - (void)testRecoverFromTLKRace {
2423 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
2424 // Test starts with nothing in CloudKit, and will fail the first TLK write.
2425 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject: ^{
2426 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2429 // Spin up CKKS subsystem.
2430 [self startCKKSSubsystem];
2432 // The first TLK write should fail, and then our fake TLKs should be there in CloudKit.
2433 // It shouldn't write anything back up to CloudKit.
2434 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2436 // Now the TLKs arrive from the other device...
2437 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2438 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2439 [self.keychainView waitForKeyHierarchyReadiness];
2441 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2442 [self addGenericPassword: @"data" account: @"account-delete-me"];
2443 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2445 // A race failure creating new TLKs should delete the old syncable one.
2446 [self checkNSyncableTLKsInKeychain: 1];
2449 - (void)testRecoverFromNullCurrentKeyPointers {
2450 // The current key pointers in cloudkit shouldn't ever not exist if keys do. But, if they don't, CKKS must recover.
2452 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
2453 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2454 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2456 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
2457 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
2458 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2459 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2460 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2462 // Spin up CKKS subsystem.
2463 [self startCKKSSubsystem];
2465 // The CKKS subsystem should figure out the issue, and fix it.
2466 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
2468 [self.keychainView waitForKeyHierarchyReadiness];
2470 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2473 - (void)testRecoverFromNoCurrentKeyPointers {
2474 // The current key pointers in cloudkit shouldn't ever point to nil. But, if they do, CKKS must recover.
2476 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
2477 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2478 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2480 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
2481 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentTLKPointer.storedCKRecord.recordID], "Deleted TLK pointer from zone");
2482 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassAPointer.storedCKRecord.recordID], "Deleted class a pointer from zone");
2483 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassCPointer.storedCKRecord.recordID], "Deleted class c pointer from zone");
2485 // Spin up CKKS subsystem.
2486 [self startCKKSSubsystem];
2488 // The CKKS subsystem should figure out the issue, and fix it.
2489 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
2491 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should have become ready");
2493 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2496 - (void)testRecoverFromBadChangeTag {
2497 // 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.
2499 // Test starts with a broken key hierarchy in our fake CloudKit, but a (incorrectly) up-to-date change tag stored locally.
2500 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2501 SecCKKSTestSetDisableKeyNotifications(true); // Don't tell CKKS about this key material; we're pretending like this is a securityd restart
2502 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2503 SecCKKSTestSetDisableKeyNotifications(false);
2505 [self.keychainView dispatchSync: ^bool {
2506 NSError* error = nil;
2507 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainZoneID.zoneName];
2508 XCTAssertNotNil(ckse, "should have received a ckse");
2510 ckse.ckzonecreated = true;
2511 ckse.ckzonesubscribed = true;
2512 ckse.changeToken = self.keychainZone.currentChangeToken;
2514 [ckse saveToDatabase: &error];
2515 XCTAssertNil(error, "shouldn't have gotten an error saving to database");
2519 // The CKKS subsystem should try to write TLKs, but fail. It'll then upload a TLK share for the keys already in CloudKit
2520 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
2521 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2523 // Spin up CKKS subsystem.
2524 [self startCKKSSubsystem];
2525 OCMVerifyAllWithDelay(self.mockDatabase, 16);
2527 // CKKS should then happily use the keys in CloudKit
2528 [self createClassCItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me"];
2529 [self createClassAItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me-class-a"];
2532 - (void)testRecoverFromDeletedKeysNewItem {
2533 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2534 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2536 [self startCKKSSubsystem];
2537 [self.keychainView waitForKeyHierarchyReadiness];
2539 // We expect a single class C record to be uploaded.
2540 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2541 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2543 [self addGenericPassword: @"data" account: @"account-delete-me"];
2544 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2546 [self waitForCKModifications];
2547 [self.keychainView waitUntilAllOperationsAreFinished];
2549 // Now, delete the local keys from the keychain (but leave the synced TLK)
2550 SecCKKSTestSetDisableKeyNotifications(true);
2551 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2552 (id)kSecClass : (id)kSecClassInternetPassword,
2553 (id)kSecAttrNoLegacy : @YES,
2554 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2555 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
2556 }), @"Deleting local keys");
2557 SecCKKSTestSetDisableKeyNotifications(false);
2559 NSError* error = nil;
2560 [self.keychainZoneKeys.classC loadKeyMaterialFromKeychain:&error];
2561 XCTAssertNotNil(error, "Error loading class C key material from keychain");
2563 // We expect a single class C record to be uploaded.
2564 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2565 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2567 [self addGenericPassword: @"datadata" account: @"account-no-keys"];
2568 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2570 // We expect a single class A record to be uploaded.
2571 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2572 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2573 [self addGenericPassword:@"asdf"
2574 account:@"account-class-A"
2576 access:(id)kSecAttrAccessibleWhenUnlocked
2577 expecting:errSecSuccess
2578 message:@"Adding class A item"];
2579 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2582 - (void)testRecoverFromDeletedKeysReceive {
2583 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2584 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2586 [self startCKKSSubsystem];
2587 [self.keychainView waitForKeyHierarchyReadiness];
2589 [self waitForCKModifications];
2590 [self.keychainView waitUntilAllOperationsAreFinished];
2592 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2594 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
2596 // Now, delete the local keys from the keychain (but leave the synced TLK)
2597 SecCKKSTestSetDisableKeyNotifications(true);
2598 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2599 (id)kSecClass : (id)kSecClassInternetPassword,
2600 (id)kSecAttrNoLegacy : @YES,
2601 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2602 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
2603 }), @"Deleting local keys");
2604 SecCKKSTestSetDisableKeyNotifications(false);
2606 // Trigger a notification (with hilariously fake data)
2607 [self.keychainZone addToZone: ckr];
2608 [self.keychainView notifyZoneChange:nil];
2609 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2610 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2612 [self findGenericPassword: @"account0" expecting:errSecSuccess];
2615 - (void)testRecoverDeletedTLK {
2616 // If the TLK disappears halfway through, well, that's no good. But we should recover using TLK sharing
2618 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2619 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2621 [self startCKKSSubsystem];
2622 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should have returned to ready");
2624 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2625 [self waitForCKModifications];
2627 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
2628 [self.keychainView waitUntilAllOperationsAreFinished];
2630 // Now, delete the local keys from the keychain
2631 SecCKKSTestSetDisableKeyNotifications(true);
2632 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2633 (id)kSecClass : (id)kSecClassInternetPassword,
2634 (id)kSecAttrNoLegacy : @YES,
2635 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2636 (id)kSecAttrSynchronizable : (id)kSecAttrSynchronizableAny,
2637 }), @"Deleting CKKS keys");
2638 SecCKKSTestSetDisableKeyNotifications(false);
2640 // Trigger a notification (with hilariously fake data)
2641 [self.keychainZone addToZone: ckr];
2642 [self.keychainView notifyZoneChange:nil];
2644 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2646 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should return to 'ready'");
2648 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // Do this again, to allow for non-atomic key state machinery switching
2650 [self findGenericPassword: @"account0" expecting:errSecSuccess];
2653 - (void)testRecoverMissingRolledKey {
2654 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2656 NSString* accountShouldExist = @"under-rolled-key";
2657 NSString* accountWillExist = @"under-rolled-key-later";
2658 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountShouldExist];
2659 [self.keychainZone addToZone: ckr];
2661 CKRecord* ckrAddedLater = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountWillExist];
2662 CKKSKey* pastClassCKey = self.keychainZoneKeys.classC;
2664 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2665 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2667 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2669 [self startCKKSSubsystem];
2670 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should have returned to ready");
2672 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2673 [self waitForCKModifications];
2675 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2676 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2677 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
2679 // Now, find and delete the class C key that ckrAddedLater is under
2680 NSError* error = nil;
2681 XCTAssertTrue([pastClassCKey deleteKeyMaterialFromKeychain:&error], "Should be able to delete old key material from keychain");
2682 XCTAssertNil(error, "Should be no error deleting old key material from keychain");
2684 [self.keychainZone addToZone:ckrAddedLater];
2685 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2687 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2688 [self findGenericPassword:accountWillExist expecting:errSecSuccess];
2690 XCTAssertTrue([pastClassCKey loadKeyMaterialFromKeychain:&error], "Class C key should be back in the keychain");
2691 XCTAssertNil(error, "Should be no error loading key from keychain");
2694 - (void)testRecoverMissingRolledClassAKeyWhileLocked {
2695 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2697 NSString* accountShouldExist = @"under-rolled-key";
2698 NSString* accountWillExist = @"under-rolled-key-later";
2699 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountShouldExist key:self.keychainZoneKeys.classA];
2700 [self.keychainZone addToZone: ckr];
2702 CKRecord* ckrAddedLater = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountWillExist key:self.keychainZoneKeys.classA];
2703 CKKSKey* pastClassAKey = self.keychainZoneKeys.classA;
2705 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2706 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2708 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2710 [self startCKKSSubsystem];
2711 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should have returned to ready");
2713 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2714 [self waitForCKModifications];
2716 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2717 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2718 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
2720 // Now, find and delete the class C key that ckrAddedLater is under
2721 NSError* error = nil;
2722 XCTAssertTrue([pastClassAKey deleteKeyMaterialFromKeychain:&error], "Should be able to delete old key material from keychain");
2723 XCTAssertNil(error, "Should be no error deleting old key material from keychain");
2725 // now, lock the keychain
2726 self.aksLockState = true;
2727 [self.lockStateTracker recheck];
2729 [self.keychainZone addToZone:ckrAddedLater];
2730 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2732 // Item should still not exist due to the lock state....
2733 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2734 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
2736 self.aksLockState = false;
2737 [self.lockStateTracker recheck];
2740 [self.keychainView waitUntilAllOperationsAreFinished];
2741 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2742 [self findGenericPassword:accountWillExist expecting:errSecSuccess];
2744 XCTAssertTrue([pastClassAKey loadKeyMaterialFromKeychain:&error], "Class A key should be back in the keychain");
2745 XCTAssertNil(error, "Should be no error loading key from keychain");
2748 - (void)testRecoverFromBadCurrentKeyPointer {
2749 // The current key pointers in cloudkit shouldn't ever point to missing entries. But, if they do, CKKS must recover.
2751 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
2752 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2753 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2755 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
2756 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
2757 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real tlk" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
2758 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class a key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
2759 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class c key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
2761 // Spin up CKKS subsystem.
2762 [self startCKKSSubsystem];
2764 // The CKKS subsystem should figure out the issue, and fix it (while uploading itself a TLK Share)
2765 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2767 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should have become ready");
2769 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2772 - (void)testRecoverFromIncorrectCurrentTLKPointer {
2773 // The current key pointers in cloudkit shouldn't ever point to wrong entries. But, if they do, CKKS must recover.
2775 // Test starts with a rolled hierarchy, and CKPs pointing to the wrong items
2776 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2777 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2779 CKKSCurrentKeyPointer* oldTLKCKP = self.keychainZoneKeys.currentTLKPointer;
2780 CKRecord* oldTLKPointer = [self.keychainZone.currentDatabase[self.keychainZoneKeys.currentTLKPointer.storedCKRecord.recordID] copy];
2782 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2783 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2785 ZoneKeys* newZoneKeys = [self.keychainZoneKeys copy];
2787 // And put the oldTLKPointer back
2788 [self.zones[self.keychainZoneID] addToZone:oldTLKPointer];
2789 self.keychainZoneKeys.currentTLKPointer = oldTLKCKP;
2791 // Make sure it stuck:
2792 XCTAssertNotEqualObjects(self.keychainZoneKeys.currentTLKPointer,
2793 newZoneKeys.currentTLKPointer,
2794 "current TLK pointer should now not point to proper TLK");
2796 // Spin up CKKS subsystem.
2797 [self startCKKSSubsystem];
2799 // The CKKS subsystem should figure out the issue, and fix it (while uploading itself a TLK Share)
2800 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
2802 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:80*NSEC_PER_SEC], "Key state should have become ready");
2804 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2805 [self waitForCKModifications];
2807 XCTAssertEqualObjects(self.keychainZoneKeys.currentTLKPointer,
2808 newZoneKeys.currentTLKPointer,
2809 "current TLK pointer should now point to proper TLK");
2810 XCTAssertEqualObjects(self.keychainZoneKeys.currentClassAPointer,
2811 newZoneKeys.currentClassAPointer,
2812 "current Class A pointer should now point to proper Class A key");
2813 XCTAssertEqualObjects(self.keychainZoneKeys.currentClassCPointer,
2814 newZoneKeys.currentClassCPointer,
2815 "current Class C pointer should now point to proper Class C key");
2818 - (void)testRecoverFromCloudKitFetchFail {
2819 // Test starts with nothing in database, but one in our fake CloudKit.
2820 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2821 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
2823 // The first two CKRecordZoneChanges should fail with a 'network unavailable' error.
2824 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
2825 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
2827 // Spin up CKKS subsystem.
2828 [self startCKKSSubsystem];
2830 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2831 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2832 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2834 // We expect a single record to be uploaded
2835 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2836 [self addGenericPassword: @"data" account: @"account-delete-me"];
2837 OCMVerifyAllWithDelay(self.mockDatabase, 12);
2839 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2840 [self addGenericPassword:@"asdf"
2841 account:@"account-class-A"
2843 access:(id)kSecAttrAccessibleWhenUnlocked
2844 expecting:errSecSuccess
2845 message:@"Adding class A item"];
2846 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2849 - (void)testRecoverFromCloudKitFetchNetworkFailAfterReady {
2850 // Test starts with nothing in database, but one in our fake CloudKit.
2851 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2853 // Spin up CKKS subsystem.
2854 [self startCKKSSubsystem];
2856 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered ready");
2857 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateReady, "CKKS entered ready");
2859 // Network is unavailable
2860 self.reachabilityFlags = 0;
2861 [self.reachabilityTracker recheck];
2863 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
2864 [self.keychainZone addToZone:ckr];
2866 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
2868 // Say network is available
2869 self.reachabilityFlags = kSCNetworkReachabilityFlagsReachable;
2870 [self.reachabilityTracker recheck];
2872 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2874 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
2877 - (void)testRecoverFromCloudKitFetchNetworkFailBeforeReady {
2878 // Test starts with nothing in database, but one in our fake CloudKit.
2879 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2881 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
2882 [self.keychainZone addToZone:ckr];
2884 // Network is unavailable
2885 self.reachabilityFlags = 0;
2886 [self.reachabilityTracker recheck];
2888 // Spin up CKKS subsystem.
2889 [self startCKKSSubsystem];
2891 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateInitializing] wait:8*NSEC_PER_SEC], "CKKS entered initializing");
2892 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateInitializing, "CKKS entered initializing");
2894 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2895 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2896 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2898 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
2900 // Say network is available
2901 self.reachabilityFlags = kSCNetworkReachabilityFlagsReachable;
2902 [self.reachabilityTracker recheck];
2904 [self.keychainView waitUntilAllOperationsAreFinished];
2905 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2907 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
2911 - (void)testRecoverFromCloudKitFetchFailWithDelay {
2912 // Test starts with nothing in database, but one in our fake CloudKit.
2913 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2914 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
2916 // The first CKRecordZoneChanges should fail with a 'delay' error.
2917 self.silentFetchesAllowed = false;
2918 [self.keychainZone failNextFetchWith:[[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorRequestRateLimited userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:4]}]];
2919 [self expectCKFetch];
2921 // Spin up CKKS subsystem.
2922 [self startCKKSSubsystem];
2924 // Ensure it doesn't fetch within these three seconds (if it does, an exception will throw).
2927 // Okay, you can fetch again.
2928 [self expectCKFetch];
2930 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2931 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2932 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2934 // We expect a single record to be uploaded
2935 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2936 [self addGenericPassword: @"data" account: @"account-delete-me"];
2937 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2940 - (void)testRecoverFromCloudKitOldChangeToken {
2941 // Test starts with nothing in database, but one in our fake CloudKit.
2942 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2944 // Spin up CKKS subsystem.
2945 [self startCKKSSubsystem];
2947 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2948 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2949 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2951 // We expect a single record to be uploaded
2952 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2953 [self addGenericPassword: @"data" account: @"account-delete-me"];
2954 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2956 // Delete all old database states, to destroy the change tag validity
2957 [self.keychainZone.pastDatabases removeAllObjects];
2959 // We expect a total local flush and refetch
2960 self.silentFetchesAllowed = false;
2961 [self expectCKFetch]; // one to fail with a CKErrorChangeTokenExpired error
2962 [self expectCKFetch]; // and one to succeed
2964 // Trigger a fake change notification
2965 [self.keychainView notifyZoneChange:nil];
2967 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2969 // And check that a new upload happens just fine.
2970 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2971 [self addGenericPassword:@"asdf"
2972 account:@"account-class-A"
2974 access:(id)kSecAttrAccessibleWhenUnlocked
2975 expecting:errSecSuccess
2976 message:@"Adding class A item"];
2977 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2980 - (void)testRecoverFromCloudKitUnknownDeviceStateRecord {
2981 // Test starts with nothing in database, but one in our fake CloudKit.
2982 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2983 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2984 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2986 // Save a new device state record with some fake etag
2987 [self.keychainView dispatchSync: ^bool {
2988 CKKSDeviceStateEntry* cdse = [[CKKSDeviceStateEntry alloc] initForDevice:self.ckDeviceID
2989 osVersion:@"fake-record"
2990 lastUnlockTime:[NSDate date]
2991 circlePeerID:self.circlePeerID
2992 circleStatus:kSOSCCInCircle
2993 keyState:SecCKKSZoneKeyStateWaitForTLK
2995 currentClassAUUID:nil
2996 currentClassCUUID:nil
2997 zoneID:self.keychainZoneID
2998 encodedCKRecord:nil];
2999 XCTAssertNotNil(cdse, "Should have created a fake CDSE");
3000 CKRecord* record = [cdse CKRecordWithZoneID:self.keychainZoneID];
3001 XCTAssertNotNil(record, "Should have created a fake CDSE CKRecord");
3002 record.etag = @"fake etag";
3003 cdse.storedCKRecord = record;
3005 NSError* error = nil;
3006 [cdse saveToDatabase:&error];
3007 XCTAssertNil(error, @"No error saving cdse to database");
3012 // Spin up CKKS subsystem.
3013 [self startCKKSSubsystem];
3015 // We expect a record failure, since the device state record is broke
3016 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
3018 // And then we expect a clean write
3019 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
3020 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3022 [self addGenericPassword: @"data" account: @"account-delete-me"];
3023 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3026 - (void)testRecoverFromCloudKitUnknownItemRecord {
3027 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3029 // Spin up CKKS subsystem.
3030 [self startCKKSSubsystem];
3032 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3034 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
3035 [self.keychainZone addToZone:ckr];
3037 [self.keychainView notifyZoneChange:nil];
3038 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3040 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3042 // Delete the record from CloudKit, but miss the notification
3043 XCTAssertNil([self.keychainZone deleteCKRecordIDFromZone: ckr.recordID], "Deleting the record from fake CloudKit should succeed");
3045 // Expect a failed upload when we modify the item
3046 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
3047 [self updateGenericPassword:@"never seen again" account:@"account-delete-me"];
3048 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3050 [self.keychainView waitUntilAllOperationsAreFinished];
3052 // And the item should be disappeared from the local keychain
3053 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3056 - (void)testRecoverFromCloudKitUserDeletedZone {
3057 // Test starts with nothing in database, but one in our fake CloudKit.
3058 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3060 // Spin up CKKS subsystem.
3061 [self startCKKSSubsystem];
3063 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3064 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3065 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3067 // We expect a single record to be uploaded
3068 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3069 [self addGenericPassword: @"data" account: @"account-delete-me"];
3070 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3072 // The first CKRecordZoneChanges should fail with a 'CKErrorUserDeletedZone' error. This will cause a local reset, ending up with zone re-creation.
3073 self.zones[self.keychainZoneID] = nil; // delete the zone
3074 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorUserDeletedZone userInfo:@{}]];
3076 // We expect CKKS to recreate the zone, then perform a key hierarchy upload, and then the class C item upload
3077 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3078 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3080 [self.keychainView notifyZoneChange:nil];
3082 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3084 // And check that a new upload occurs.
3085 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3087 [self addGenericPassword:@"asdf"
3088 account:@"account-class-A"
3090 access:(id)kSecAttrAccessibleWhenUnlocked
3091 expecting:errSecSuccess
3092 message:@"Adding class A item"];
3093 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3096 - (void)testRecoverFromCloudKitZoneNotFoundWithoutZoneDeletion {
3097 // Test starts with nothing in database, but one in our fake CloudKit.
3098 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3099 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
3101 // Spin up CKKS subsystem.
3102 [self startCKKSSubsystem];
3104 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3105 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3106 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3108 // We expect a single record to be uploaded
3109 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3110 [self addGenericPassword: @"data" account: @"account-delete-me"];
3111 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3113 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS should enter 'ready'");
3115 [self waitForCKModifications];
3116 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
3118 // The next CKRecordZoneChanges will fail with a 'zone not found' error.
3119 self.zones[self.keychainZoneID] = nil; // delete the zone
3121 // We expect CKKS to reset itself and recover, then a key hierarchy upload, and then the class C item upload
3122 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3123 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3125 [self.keychainView notifyZoneChange:nil];
3126 OCMVerifyAllWithDelay(self.mockDatabase, 80);
3127 [self waitForCKModifications];
3129 // And check that a new upload occurs.
3130 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3132 [self addGenericPassword:@"asdf"
3133 account:@"account-class-A"
3135 access:(id)kSecAttrAccessibleWhenUnlocked
3136 expecting:errSecSuccess
3137 message:@"Adding class A item"];
3138 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3141 - (void)testRecoverFromCloudKitZoneNotFoundFetchBeforeSigninOccurs {
3142 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
3144 // Before CKKS sign-in, it receives a fetch rpc
3145 XCTestExpectation *fetchReturns = [self expectationWithDescription:@"fetch returned"];
3146 [self.injectedManager rpcFetchAndProcessChanges:nil reply:^(NSError *result) {
3147 XCTAssertNil(result, "Should be no error fetching and processing changes");
3148 [fetchReturns fulfill];
3151 // start 'login'. CKKS Should upload a key hierarchy
3152 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
3153 [self startCKKSSubsystem];
3155 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS should enter 'ready'");
3157 // We expect a single record to be uploaded
3158 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
3159 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3160 [self addGenericPassword: @"data" account: @"account-delete-me"];
3161 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3163 // The fetch should have come back by now
3164 [self waitForExpectations: @[fetchReturns] timeout:5];
3167 - (void)testNoCloudKitAccount {
3168 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
3169 self.accountStatus = CKAccountStatusNoAccount;
3170 self.circleStatus = kSOSCCNotInCircle;
3171 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3173 self.silentFetchesAllowed = false;
3174 [self startCKKSSubsystem];
3176 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3178 [self addGenericPassword: @"data" account: @"account-delete-me"];
3179 [self.keychainView waitUntilAllOperationsAreFinished];
3181 // simulate a NSNotification callback (but still logged out)
3182 self.accountStatus = CKAccountStatusNoAccount;
3183 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3185 // There should be no further uploads, even when we save keychain items
3186 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3187 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3189 [self.keychainView waitUntilAllOperationsAreFinished];
3190 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3192 // Test that there are no items in the database (since we never logged in)
3193 [self checkNoCKKSData: self.keychainView];
3196 - (void)testSACloudKitAccount {
3197 // Test starts with nothing in database and the user logged into CloudKit and in circle, but the account is not HSA2.
3198 self.circleStatus = kSOSCCInCircle;
3199 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3201 self.accountStatus = CKAccountStatusAvailable;
3202 self.supportsDeviceToDeviceEncryption = NO;
3204 self.silentFetchesAllowed = false;
3205 [self startCKKSSubsystem];
3207 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3208 XCTAssertNotNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's no account");
3209 XCTAssertEqualObjects(self.accountStateTracker.currentAccountError.domain, CKKSErrorDomain, "Account tracker error should be in CKKSErrorDomain");
3210 XCTAssertEqual(self.accountStateTracker.currentAccountError.code, CKKSNotHSA2, "Account tracker error should be upset about HSA2");
3212 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3214 // There should be no uploads, even when we save keychain items and enter/exit circle
3215 [self addGenericPassword: @"data" account: @"account-delete-me"];
3216 [self.keychainView waitUntilAllOperationsAreFinished];
3218 self.circleStatus = kSOSCCNotInCircle;
3219 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3220 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3222 self.circleStatus = kSOSCCInCircle;
3223 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3224 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3226 [self.keychainView waitUntilAllOperationsAreFinished];
3227 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3229 // Test that there are no items in the database (since we never were in an HSA2 account)
3230 [self checkNoCKKSData: self.keychainView];
3233 - (void)testNoCircle {
3234 // Test starts with nothing in database and the user logged into CloudKit, but out of Circle.
3235 self.circleStatus = kSOSCCNotInCircle;
3236 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3238 self.accountStatus = CKAccountStatusAvailable;
3240 self.silentFetchesAllowed = false;
3241 [self startCKKSSubsystem];
3243 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3244 XCTAssertNotNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's no account");
3245 XCTAssertEqualObjects(self.accountStateTracker.currentAccountError.domain, (__bridge NSString*)kSOSErrorDomain, "Account tracker error should be in SOSErrorDomain");
3246 XCTAssertEqual(self.accountStateTracker.currentAccountError.code, kSOSErrorNotInCircle, "Account tracker error should be upset about out-of-circle");
3248 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3250 [self addGenericPassword: @"data" account: @"account-delete-me"];
3251 [self.keychainView waitUntilAllOperationsAreFinished];
3253 // simulate a NSNotification callback (but still logged out)
3254 self.accountStatus = CKAccountStatusNoAccount;
3255 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3257 // There should be no further uploads, even when we save keychain items
3258 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3259 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3261 [self.keychainView waitUntilAllOperationsAreFinished];
3262 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3264 // Test that there are no items in the database (since we never logged in)
3265 [self checkNoCKKSData: self.keychainView];
3268 - (void)testCloudKitLogin {
3269 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
3270 self.accountStatus = CKAccountStatusNoAccount;
3271 self.circleStatus = kSOSCCNotInCircle;
3272 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3274 // Before we inform CKKS of its account state....
3275 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:10*NSEC_PER_MSEC], "CKK shouldn't know the account state");
3277 [self startCKKSSubsystem];
3279 XCTAssertEqual(0, [self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], "Should have been told of a 'logout' event on startup");
3280 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event shouldn't have happened");
3281 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:10*NSEC_PER_MSEC], "CKK should know the account state");
3283 [self.keychainView waitUntilAllOperationsAreFinished];
3284 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3286 XCTAssertNotNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's no account");
3287 XCTAssertEqualObjects(self.accountStateTracker.currentAccountError.domain, CKKSErrorDomain, "Account tracker error should be in CKKSErrorDomain");
3288 XCTAssertEqual(self.accountStateTracker.currentAccountError.code, CKKSNotLoggedIn, "Account tracker error should just be 'no account'");
3290 // simulate a cloudkit login and NSNotification callback
3291 self.accountStatus = CKAccountStatusAvailable;
3292 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3294 XCTAssertNotNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's no account");
3295 XCTAssertEqualObjects(self.accountStateTracker.currentAccountError.domain, (__bridge NSString*)kSOSErrorDomain, "Account tracker error should be in SOSErrorDomain");
3296 XCTAssertEqual(self.accountStateTracker.currentAccountError.code, kSOSErrorNotInCircle, "Account tracker error should be upset about out-of-circle");
3298 // No writes yet, since we're not in circle
3299 [self.keychainView waitUntilAllOperationsAreFinished];
3300 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3302 // We expect some sort of TLK/key hierarchy upload once we are notified of entering the circle.
3303 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3305 self.circleStatus = kSOSCCInCircle;
3306 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3308 XCTAssertNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's an account");
3310 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3311 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3312 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:10*NSEC_PER_MSEC], "CKK should know the account state");
3314 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3315 [self waitForCKModifications];
3317 // We expect a single class C record to be uploaded.
3318 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3319 [self addGenericPassword: @"data" account: @"account-delete-me"];
3321 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3322 [self waitForCKModifications];
3325 - (void)testCloudKitLogoutLogin {
3326 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
3327 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3329 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:10*NSEC_PER_MSEC], "CKK shouldn't know the account state");
3330 [self startCKKSSubsystem];
3331 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3332 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3333 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:10*NSEC_PER_MSEC], "CKK should know the account state");
3335 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3336 [self waitForCKModifications];
3338 // We expect a single class C record to be uploaded.
3339 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3340 [self addGenericPassword: @"data" account: @"account-delete-me"];
3342 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3343 [self waitForCKModifications];
3345 // simulate a cloudkit logout and NSNotification callback
3346 self.accountStatus = CKAccountStatusNoAccount;
3347 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3348 self.circleStatus = kSOSCCNotInCircle;
3349 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3351 XCTAssertNotNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's no account");
3352 XCTAssertEqualObjects(self.accountStateTracker.currentAccountError.domain, CKKSErrorDomain, "Account tracker error should be in CKKSErrorDomain");
3353 XCTAssertEqual(self.accountStateTracker.currentAccountError.code, CKKSNotLoggedIn, "Account tracker error should just believe we're not logged in");
3355 // Test that there are no items in the database after logout
3356 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
3357 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
3358 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:10*NSEC_PER_MSEC], "CKK should know the account state");
3359 [self checkNoCKKSData: self.keychainView];
3361 // There should be no further uploads, even when we save keychain items
3362 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3363 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3365 [self.keychainView waitUntilAllOperationsAreFinished];
3366 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3367 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:8*NSEC_PER_SEC], "CKKS entered 'logged out'");
3369 // simulate a cloudkit login
3370 // 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
3371 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
3373 self.accountStatus = CKAccountStatusAvailable;
3374 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3375 self.circleStatus = kSOSCCInCircle;
3376 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3377 XCTAssertNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's an account");
3379 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3380 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3381 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:10*NSEC_PER_MSEC], "CKK should know the account state");
3383 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3385 // Let everything settle...
3386 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready'");
3387 [self waitForCKModifications];
3390 self.accountStatus = CKAccountStatusNoAccount;
3391 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3392 self.circleStatus = kSOSCCNotInCircle;
3393 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3395 // Test that there are no items in the database after logout
3396 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
3397 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
3398 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:10*NSEC_PER_MSEC], "CKK should know the account state");
3399 [self checkNoCKKSData: self.keychainView];
3401 // There should be no further uploads, even when we save keychain items
3402 [self addGenericPassword: @"data" account: @"account-delete-me-5"];
3403 [self addGenericPassword: @"data" account: @"account-delete-me-6"];
3405 [self.keychainView waitUntilAllOperationsAreFinished];
3406 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3408 // simulate a cloudkit login
3409 // 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
3410 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
3412 self.accountStatus = CKAccountStatusAvailable;
3413 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3414 self.circleStatus = kSOSCCInCircle;
3415 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3417 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3418 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3419 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:10*NSEC_PER_MSEC], "CKK should know the account state");
3421 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3423 // Let everything settle...
3424 [self.keychainView waitUntilAllOperationsAreFinished];
3425 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready'");
3428 self.accountStatus = CKAccountStatusNoAccount;
3429 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3430 self.circleStatus = kSOSCCNotInCircle;
3431 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3433 // Test that there are no items in the database after logout
3434 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
3435 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
3436 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:10*NSEC_PER_MSEC], "CKK should know the account state");
3437 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:8*NSEC_PER_SEC], "CKKS entered 'logged out'");
3438 [self checkNoCKKSData: self.keychainView];
3440 // Force zone into error state
3441 self.keychainView.keyHierarchyState = SecCKKSZoneKeyStateError;
3443 self.accountStatus = CKAccountStatusAvailable;
3444 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3445 self.circleStatus = kSOSCCInCircle;
3446 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3448 XCTestExpectation *operationRun = [self expectationWithDescription:@"operation run"];
3449 NSOperation* op = [NSBlockOperation named:@"test" withBlock:^{
3450 [operationRun fulfill];
3453 [op addDependency:self.keychainView.keyStateReadyDependency];
3454 [self.operationQueue addOperation:op];
3456 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3457 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3458 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:10*NSEC_PER_MSEC], "CKK should know the account state");
3460 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3461 [self waitForExpectations: @[operationRun] timeout:5];
3462 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready'");
3465 - (void)testCloudKitLogoutDueToGreyMode {
3466 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
3467 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
3469 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:10*NSEC_PER_MSEC], "CKK shouldn't know the account state");
3470 [self startCKKSSubsystem];
3471 XCTAssertEqual(0, [self.keychainView.loggedIn wait:8*NSEC_PER_SEC], "Should have been told of a 'login'");
3472 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:10*NSEC_PER_MSEC], "'logout' event should be reset");
3473 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:10*NSEC_PER_MSEC], "CKK should know the account state");
3475 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
3477 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3478 [self waitForCKModifications];
3480 // simulate a cloudkit grey mode switch and NSNotification callback. CKKS should treat this as a logout
3481 self.iCloudHasValidCredentials = false;
3482 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3484 XCTAssertNotNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's no account");
3485 XCTAssertEqualObjects(self.accountStateTracker.currentAccountError.domain, CKKSErrorDomain, "Account tracker error should be in CKKSErrorDomain");
3486 XCTAssertEqual(self.accountStateTracker.currentAccountError.code, CKKSiCloudGreyMode, "Account tracker error should be upset about grey mode");
3488 // Test that there are no items in the database after logout
3489 XCTAssertEqual(0, [self.keychainView.loggedOut wait:8*NSEC_PER_SEC], "Should have been told of a 'logout'");
3490 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:10*NSEC_PER_MSEC], "'login' event should be reset");
3491 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:10*NSEC_PER_MSEC], "CKK should know the account state");
3492 [self checkNoCKKSData: self.keychainView];
3493 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:8*NSEC_PER_SEC], "CKKS entered 'logged out'");
3495 // There should be no further uploads, even when we save keychain items
3496 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3497 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3499 [self.keychainView waitUntilAllOperationsAreFinished];
3500 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3502 // Also, fetches shouldn't occur
3503 self.silentFetchesAllowed = false;
3504 NSOperation* op = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
3505 CKKSResultOperation* timeoutOp = [CKKSResultOperation named:@"timeout" withBlock:^{}];
3506 [timeoutOp addDependency:op];
3507 [timeoutOp timeout:4*NSEC_PER_SEC];
3508 [self.operationQueue addOperation:timeoutOp];
3509 [timeoutOp waitUntilFinished];
3511 // CloudKit figures its life out. We expect the two passwords from before to be uploaded
3512 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
3513 self.silentFetchesAllowed = true;
3514 self.iCloudHasValidCredentials = true;
3515 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3517 XCTAssertNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's an account");
3519 XCTAssertEqual(0, [self.keychainView.loggedIn wait:8*NSEC_PER_SEC], "Should have been told of a 'login'");
3520 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:10*NSEC_PER_MSEC], "'logout' event should be reset");
3521 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:10*NSEC_PER_MSEC], "CKK should know the account state");
3522 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3524 // And fetching still works!
3525 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D00" withAccount:@"account0"]];
3526 [self.keychainView notifyZoneChange:nil];
3527 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3528 [self findGenericPassword: @"account0" expecting:errSecSuccess];
3529 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready'");
3532 - (void)testCloudKitLoginRace {
3533 // Test starts with nothing in database, and 'in circle', but securityd hasn't received notification if we're logged into CloudKit.
3534 // CKKS should not call handleLogout.
3536 id partialKVMock = OCMPartialMock(self.keychainView);
3537 OCMReject([partialKVMock handleCKLogout]);
3538 // note: don't unblock the ck account state object yet...
3540 self.circleStatus = kSOSCCInCircle;
3541 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3543 // Add a keychain item, but make sure it doesn't upload yet.
3544 [self addGenericPassword: @"data" account: @"account-delete-me"];
3546 [self.keychainView waitUntilAllOperationsAreFinished];
3547 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3549 // Now that we're here (and handleCKLogout hasn't been called), bring the account up
3551 // We expect some sort of TLK/key hierarchy upload once we are notified of entering the circle.
3552 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3554 // We expect a single class C record to be uploaded.
3555 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3557 self.accountStatus = CKAccountStatusAvailable;
3558 [self startCKAccountStatusMock];
3560 // simulate another NSNotification callback
3561 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3563 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3564 [self waitForCKModifications];
3566 // Make sure new items upload too
3567 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3568 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3569 OCMVerifyAllWithDelay(self.mockDatabase, 8);
3571 [self.keychainView waitUntilAllOperationsAreFinished];
3572 [self waitForCKModifications];
3573 [self.keychainView halt];
3575 [partialKVMock stopMocking];
3578 - (void)testNotStuckAfterReset {
3579 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3581 XCTestExpectation *operationRun = [self expectationWithDescription:@"operation run"];
3582 NSOperation* op = [NSBlockOperation named:@"test" withBlock:^{
3583 [operationRun fulfill];
3586 [op addDependency:self.keychainView.keyStateReadyDependency];
3587 [self.operationQueue addOperation:op];
3589 // And handle a spurious logout
3590 [self.keychainView handleCKLogout];
3592 [self startCKKSSubsystem];
3594 [self waitForExpectations: @[operationRun] timeout:80];
3597 - (void)testCKKSControlBringup {
3598 NSXPCInterface *interface = CKKSSetupControlProtocol([NSXPCInterface interfaceWithProtocol:@protocol(CKKSControlProtocol)]);
3599 XCTAssertNotNil(interface, "Received a configured CKKS interface");