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"
54 #import "keychain/categories/NSError+UsefulConstructors.h"
56 #import "keychain/ckks/tests/MockCloudKit.h"
58 #import "keychain/ckks/tests/CKKSTests.h"
61 @interface CKKSLockStateTracker ()
62 @property (nullable) NSDate* lastUnlockedTime;
65 @implementation CloudKitKeychainSyncingTests
69 - (void)testBringupToKeyStateReady {
70 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
71 [self startCKKSSubsystem];
73 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
77 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
79 // We expect a single record to be uploaded.
80 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
82 [self startCKKSSubsystem];
83 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
85 [self addGenericPassword: @"data" account: @"account-delete-me"];
87 OCMVerifyAllWithDelay(self.mockDatabase, 20);
90 - (void)testActiveTLKS {
91 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
93 // We expect a single record to be uploaded.
94 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
96 [self startCKKSSubsystem];
98 [self addGenericPassword: @"data" account: @"account-delete-me"];
100 OCMVerifyAllWithDelay(self.mockDatabase, 20);
102 NSDictionary<NSString *,NSString *>* tlks = [[CKKSViewManager manager] activeTLKs];
104 XCTAssertEqual([tlks count], (NSUInteger)1, "One TLK");
105 XCTAssertNotNil(tlks[@"keychain"], "keychain have a UUID");
109 - (void)testAddMultipleItems {
110 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
111 [self startCKKSSubsystem];
113 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
114 [self addGenericPassword: @"data" account: @"account-delete-me"];
115 OCMVerifyAllWithDelay(self.mockDatabase, 20);
117 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
118 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
119 OCMVerifyAllWithDelay(self.mockDatabase, 20);
121 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
122 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
123 OCMVerifyAllWithDelay(self.mockDatabase, 20);
126 - (void)testAddItemWithoutUUID {
127 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
128 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
129 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
130 [self saveTLKMaterialToKeychain:self.keychainZoneID];
132 [self startCKKSSubsystem];
134 [self.keychainView waitUntilAllOperationsAreFinished];
136 SecCKKSTestSetDisableAutomaticUUID(true);
137 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
139 SecCKKSTestSetDisableAutomaticUUID(false);
141 // We then expect an upload of the added item
142 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
144 OCMVerifyAllWithDelay(self.mockDatabase, 20);
147 - (void)testModifyItem {
148 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
150 NSString* account = @"account-delete-me";
152 [self startCKKSSubsystem];
154 // We expect a single record to be uploaded.
155 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
156 [self addGenericPassword: @"data" account: account];
157 OCMVerifyAllWithDelay(self.mockDatabase, 20);
159 // And then modified.
160 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
161 [self updateGenericPassword: @"otherdata" account:account];
162 OCMVerifyAllWithDelay(self.mockDatabase, 20);
165 - (void)testModifyItemImmediately {
166 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
167 NSString* account = @"account-delete-me";
169 [self startCKKSSubsystem];
170 [self holdCloudKitModifications];
172 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
173 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
174 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
175 [self addGenericPassword: @"data" account: account];
176 OCMVerifyAllWithDelay(self.mockDatabase, 20);
178 // Right now, the write in CloudKit is pending. Make the local modification...
179 [self updateGenericPassword: @"otherdata" account:account];
181 // And then schedule the update
182 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
183 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
184 [self releaseCloudKitModificationHold];
186 OCMVerifyAllWithDelay(self.mockDatabase, 20);
189 - (void)testModifyItemPrimaryKey {
190 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
192 NSString* account = @"account-delete-me";
194 [self startCKKSSubsystem];
196 // We expect a single record to be uploaded.
197 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
198 [self addGenericPassword: @"data" account: account];
199 OCMVerifyAllWithDelay(self.mockDatabase, 20);
201 // And then modified. Since we're changing the "primary key", we expect to delete the old record and upload a new one.
202 [self expectCKModifyItemRecords:1 deletedRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:nil];
203 [self updateAccountOfGenericPassword: @"new-account-delete-me" account:account];
204 OCMVerifyAllWithDelay(self.mockDatabase, 20);
207 - (void)testModifyItemDuringReencrypt {
208 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
209 NSString* account = @"account-delete-me";
211 [self startCKKSSubsystem];
212 [self holdCloudKitModifications];
214 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
215 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
216 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
217 [self addGenericPassword: @"data" account: account];
218 OCMVerifyAllWithDelay(self.mockDatabase, 20);
220 // Right now, the write in CloudKit is pending. Make the local modification...
221 [self updateGenericPassword: @"otherdata" account:account];
223 // And then schedule the update
224 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
225 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
227 // Stop the reencrypt operation from happening
228 self.keychainView.holdReencryptOutgoingItemsOperation = [CKKSGroupOperation named:@"reencrypt-hold" withBlock: ^{
229 secnotice("ckks", "releasing reencryption hold");
232 // The cloudkit operation finishes, letting the next OQO proceed (and set up the reencryption operation)
233 [self releaseCloudKitModificationHold];
235 // And wait for this to finish...
236 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
237 // And once more to quiesce.
238 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
240 // Pause outgoing queue operations to ensure the reencryption operation runs first
241 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
242 secnotice("ckks", "releasing outgoing-queue hold");
245 // Run the reencrypt items operation to completion.
246 [self.operationQueue addOperation: self.keychainView.holdReencryptOutgoingItemsOperation];
247 [self.keychainView waitForOperationsOfClass:[CKKSReencryptOutgoingItemsOperation class]];
249 [self.operationQueue addOperation: self.keychainView.holdOutgoingQueueOperation];
251 OCMVerifyAllWithDelay(self.mockDatabase, 20);
252 [self.keychainView waitUntilAllOperationsAreFinished];
253 [self waitForCKModifications];
256 - (void)testModifyItemBeforeReencrypt {
257 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
258 NSString* account = @"account-delete-me";
260 [self startCKKSSubsystem];
261 [self holdCloudKitModifications];
263 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
264 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
265 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
266 [self addGenericPassword: @"data" account: account];
267 OCMVerifyAllWithDelay(self.mockDatabase, 20);
269 // Right now, the write in CloudKit is pending. Make the local modification...
270 [self updateGenericPassword: @"otherdata" account:account];
272 // And then schedule the update, but for the final version of the password
273 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
274 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"third"]];
276 // Stop the reencrypt operation from happening
277 self.keychainView.holdReencryptOutgoingItemsOperation = [CKKSGroupOperation named:@"reencrypt-hold" withBlock: ^{
278 secnotice("ckks", "releasing reencryption hold");
281 // The cloudkit operation finishes, letting the next OQO proceed (and set up the reencryption operation)
282 [self releaseCloudKitModificationHold];
284 // And wait for this to finish...
285 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
286 // And once more to quiesce.
287 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
289 [self updateGenericPassword: @"third" account:account];
291 // Item should upload.
292 OCMVerifyAllWithDelay(self.mockDatabase, 20);
294 // Run the reencrypt items operation to completion.
295 [self.operationQueue addOperation: self.keychainView.holdReencryptOutgoingItemsOperation];
296 [self.keychainView waitForOperationsOfClass:[CKKSReencryptOutgoingItemsOperation class]];
298 [self.keychainView waitUntilAllOperationsAreFinished];
299 [self waitForCKModifications];
302 - (void)testModifyItemDuringNetworkFailure {
303 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
304 NSString* account = @"account-delete-me";
306 [self startCKKSSubsystem];
307 [self holdCloudKitModifications];
309 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
310 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
312 [self addGenericPassword: @"data" account: account];
313 OCMVerifyAllWithDelay(self.mockDatabase, 20);
315 // Right now, the write in CloudKit is pending. Make the local modification...
316 [self updateGenericPassword: @"otherdata" account:account];
318 // And then schedule the update, but for the final version of the password
319 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
320 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
322 // The cloudkit operation finishes, letting the next OQO proceed (and set up uploading the new item)
323 [self releaseCloudKitModificationHold];
325 // Item should upload.
326 OCMVerifyAllWithDelay(self.mockDatabase, 20);
328 [self.keychainView waitUntilAllOperationsAreFinished];
329 [self waitForCKModifications];
332 - (void)testOutgoingQueueRecoverFromStaleInflightEntry {
333 // CKKS is restarting with an existing in-flight OQE
334 // Note that this test is incomplete, and doesn't re-add the item to the local keychain
335 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
336 NSString* account = @"fake-account";
338 [self.keychainView dispatchSync:^bool {
339 NSError* error = nil;
341 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
343 CKKSItem* item = [self newItem:ckrid withNewItemData:[self fakeRecordDictionary:account zoneID:self.keychainZoneID] key:self.keychainZoneKeys.classC];
344 XCTAssertNotNil(item, "Should be able to create a new fake item");
346 CKKSOutgoingQueueEntry* oqe = [[CKKSOutgoingQueueEntry alloc] initWithCKKSItem:item action:SecCKKSActionAdd state:SecCKKSStateInFlight waitUntil:nil accessGroup:@"ckks"];
347 XCTAssertNotNil(oqe, "Should be able to create a new fake OQE");
348 [oqe saveToDatabase:&error];
350 XCTAssertNil(error, "Shouldn't error saving new OQE to database");
354 NSError *error = NULL;
355 XCTAssertEqual([CKKSOutgoingQueueEntry countByState:SecCKKSStateInFlight zone:self.keychainZoneID error:&error], 1,
356 "Expected on inflight entry in outgoing queue: %@", error);
358 // When CKKS restarts, it should find and re-upload this item
359 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
360 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
362 [self startCKKSSubsystem];
363 [self.keychainView waitForFetchAndIncomingQueueProcessing];
365 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
366 [self.keychainView waitForKeyHierarchyReadiness];
367 OCMVerifyAllWithDelay(self.mockDatabase, 20);
370 - (void)testOutgoingQueueRecoverFromNetworkFailure {
371 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
372 NSString* account = @"account-delete-me";
374 [self startCKKSSubsystem];
375 [self holdCloudKitModifications];
377 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
379 NSError* greyMode = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNotAuthenticated userInfo:@{}];
380 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:greyMode];
382 [self addGenericPassword: @"data" account: account];
383 OCMVerifyAllWithDelay(self.mockDatabase, 20);
385 // And then schedule the retried update
386 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
387 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
389 // The cloudkit operation finishes, letting the next OQO proceed (and set up uploading the new item)
390 [self releaseCloudKitModificationHold];
392 OCMVerifyAllWithDelay(self.mockDatabase, 20);
394 [self.keychainView waitUntilAllOperationsAreFinished];
395 [self waitForCKModifications];
398 - (void)testDeleteItem {
399 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
401 [self startCKKSSubsystem];
403 // We expect a single record to be uploaded.
404 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
405 [self addGenericPassword: @"data" account: @"account-delete-me"];
406 OCMVerifyAllWithDelay(self.mockDatabase, 20);
408 // We expect a single record to be deleted.
409 [self expectCKDeleteItemRecords: 1 zoneID:self.keychainZoneID];
410 [self deleteGenericPassword:@"account-delete-me"];
411 OCMVerifyAllWithDelay(self.mockDatabase, 20);
414 - (void)testDeleteItemImmediatelyAfterModify {
415 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
416 NSString* account = @"account-delete-me";
418 [self startCKKSSubsystem];
420 // We expect a single record to be uploaded.
421 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
422 [self addGenericPassword: @"data" account: account];
423 OCMVerifyAllWithDelay(self.mockDatabase, 20);
425 // Now, hold the modify
426 [self holdCloudKitModifications];
428 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
429 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
430 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
432 [self updateGenericPassword: @"otherdata" account:account];
433 OCMVerifyAllWithDelay(self.mockDatabase, 20);
435 // Right now, the write in CloudKit is pending. Make the local deletion...
436 [self deleteGenericPassword:account];
438 // And then schedule the update
439 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
440 [self releaseCloudKitModificationHold];
442 OCMVerifyAllWithDelay(self.mockDatabase, 20);
445 - (void)testDeleteItemAfterFetchAfterModify {
446 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
447 NSString* account = @"account-delete-me";
449 [self startCKKSSubsystem];
451 // We expect a single record to be uploaded.
452 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
453 [self addGenericPassword: @"data" account: account];
454 OCMVerifyAllWithDelay(self.mockDatabase, 20);
456 // Now, hold the modify
457 //[self holdCloudKitModifications];
459 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
460 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
461 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
463 [self updateGenericPassword: @"otherdata" account:account];
464 OCMVerifyAllWithDelay(self.mockDatabase, 20);
466 // Right now, the write in CloudKit is pending. Place a hold on outgoing queue processing
467 // Place a hold on processing the outgoing queue.
468 CKKSResultOperation* blockOutgoing = [CKKSResultOperation operationWithBlock:^{
469 secnotice("ckks", "Outgoing queue hold released.");
471 blockOutgoing.name = @"outgoing-queue-hold";
472 CKKSResultOperation* outgoingQueueOperation = [self.keychainView processOutgoingQueueAfter:blockOutgoing ckoperationGroup:nil];
474 [self deleteGenericPassword:account];
476 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
478 // Release the CK modification hold
479 //[self releaseCloudKitModificationHold];
482 [self.keychainView waitForFetchAndIncomingQueueProcessing];
483 [self.operationQueue addOperation:blockOutgoing];
484 [outgoingQueueOperation waitUntilFinished];
486 OCMVerifyAllWithDelay(self.mockDatabase, 20);
490 - (void)testReceiveItem {
491 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
492 [self startCKKSSubsystem];
494 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
495 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
496 (id)kSecAttrAccount : @"account-delete-me",
497 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
498 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
501 CFTypeRef item = NULL;
502 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
504 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
505 [self.keychainZone addToZone: ckr];
507 // Trigger a notification (with hilariously fake data)
508 [self.keychainView notifyZoneChange:nil];
510 [self.keychainView waitForFetchAndIncomingQueueProcessing];
511 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
514 - (void)testReceiveManyItems {
515 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
516 [self startCKKSSubsystem];
518 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D00" withAccount:@"account0"]];
519 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D01" withAccount:@"account1"]];
520 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D02" withAccount:@"account2"]];
521 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D03" withAccount:@"account3"]];
522 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D04" withAccount:@"account4"]];
523 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D05" withAccount:@"account5"]];
524 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D06" withAccount:@"account6"]];
525 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D07" withAccount:@"account7"]];
526 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D08" withAccount:@"account8"]];
527 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D09" withAccount:@"account9"]];
528 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D10" withAccount:@"account10"]];
529 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D11" withAccount:@"account11"]];
531 for(int i = 12; i < 100; i++) {
533 NSString* recordName = [NSString stringWithFormat:@"7B598D31-F9C5-481E-98AC-%012d", i];
534 NSString* account = [NSString stringWithFormat:@"account%d", i];
536 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:recordName withAccount:account]];
540 // Trigger a notification (with hilariously fake data)
541 [self.keychainView notifyZoneChange:nil];
543 [self.keychainView waitForFetchAndIncomingQueueProcessing];
545 [self findGenericPassword: @"account0" expecting:errSecSuccess];
546 [self findGenericPassword: @"account1" expecting:errSecSuccess];
547 [self findGenericPassword: @"account2" expecting:errSecSuccess];
548 [self findGenericPassword: @"account3" expecting:errSecSuccess];
549 [self findGenericPassword: @"account4" expecting:errSecSuccess];
550 [self findGenericPassword: @"account5" expecting:errSecSuccess];
551 [self findGenericPassword: @"account6" expecting:errSecSuccess];
552 [self findGenericPassword: @"account7" expecting:errSecSuccess];
553 [self findGenericPassword: @"account8" expecting:errSecSuccess];
554 [self findGenericPassword: @"account9" expecting:errSecSuccess];
555 [self findGenericPassword: @"account10" expecting:errSecSuccess];
556 [self findGenericPassword: @"account11" expecting:errSecSuccess];
559 - (void)testReceiveCollidingItem {
560 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
561 [self startCKKSSubsystem];
563 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
564 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
565 (id)kSecAttrAccount : @"account-delete-me",
566 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
567 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
570 CFTypeRef item = NULL;
571 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
573 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
574 CKRecord* ckr2 = [self createFakeRecord: self.keychainZoneID recordName: @"F9C58D31-7B59-481E-98AC-5A507ACB2D85"];
576 [self.keychainZone addToZone: ckr];
577 [self.keychainZone addToZone: ckr2];
579 // We expect a delete operation with the "higher" UUID.
580 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
582 // Trigger a notification (with hilariously fake data)
583 [self.keychainView notifyZoneChange:nil];
585 OCMVerifyAllWithDelay(self.mockDatabase, 20);
586 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
588 [self waitForCKModifications];
589 XCTAssertNil(self.keychainZone.currentDatabase[ckr2.recordID], "Correct record was deleted from CloudKit");
592 -(void)testReceiveItemDelete {
593 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
594 [self startCKKSSubsystem];
596 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
597 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
598 (id)kSecAttrAccount : @"account-delete-me",
599 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
600 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
603 CFTypeRef item = NULL;
604 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
606 [self.keychainView waitForFetchAndIncomingQueueProcessing];
608 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
609 [self.keychainZone addToZone: ckr];
611 // Trigger a notification (with hilariously fake data)
612 [self.keychainView notifyZoneChange:nil];
613 [self.keychainView waitForFetchAndIncomingQueueProcessing];
615 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
619 [self.keychainZone deleteCKRecordIDFromZone: [ckr recordID]];
620 [self.keychainView notifyZoneChange:nil];
621 [self.keychainView waitForFetchAndIncomingQueueProcessing];
623 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
626 -(void)testReceiveItemPhantomDelete {
627 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
628 [self startCKKSSubsystem];
630 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
631 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
632 (id)kSecAttrAccount : @"account-delete-me",
633 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
634 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
637 CFTypeRef item = NULL;
638 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
640 [self.keychainView waitForFetchAndIncomingQueueProcessing];
642 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
643 [self.keychainZone addToZone: ckr];
645 // Trigger a notification (with hilariously fake data)
646 [self.keychainView notifyZoneChange:nil];
647 [self.keychainView waitForFetchAndIncomingQueueProcessing];
649 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
652 [self.keychainView waitUntilAllOperationsAreFinished];
655 [self.keychainZone deleteCKRecordIDFromZone: [ckr recordID]];
657 // and add another, incorrect IQE
658 [self.keychainView dispatchSync: ^bool {
659 // Inefficient, but hey, it works
660 CKRecord* record = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-FFFF-FFFF-5A507ACB2D85"];
661 CKKSItem* fakeItem = [[CKKSItem alloc] initWithCKRecord: record];
663 CKKSIncomingQueueEntry* iqe = [[CKKSIncomingQueueEntry alloc] initWithCKKSItem:fakeItem
664 action:SecCKKSActionDelete
665 state:SecCKKSStateNew];
666 XCTAssertNotNil(iqe, "could create fake IQE");
667 NSError* error = nil;
668 XCTAssert([iqe saveToDatabase: &error], "Saved fake IQE to database");
669 XCTAssertNil(error, "No error saving fake IQE to database");
673 [self.keychainView notifyZoneChange:nil];
674 [self.keychainView waitForFetchAndIncomingQueueProcessing];
676 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
678 // The incoming queue should be empty
679 [self.keychainView dispatchSync: ^bool {
680 NSError* error = nil;
681 NSArray* iqes = [CKKSIncomingQueueEntry all:&error];
682 XCTAssertNil(error, "No error loading IQEs");
683 XCTAssertNotNil(iqes, "Could load IQEs");
684 XCTAssertEqual(iqes.count, 0u, "Incoming queue is empty");
688 -(void)testReceiveConflictOnJustAddedItem {
689 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
690 [self startCKKSSubsystem];
692 [self.keychainView waitForKeyHierarchyReadiness];
693 [self.keychainView waitUntilAllOperationsAreFinished];
695 // Place a hold on processing the outgoing queue.
696 CKKSResultOperation* blockOutgoing = [CKKSResultOperation operationWithBlock:^{
697 secnotice("ckks", "Outgoing queue hold released.");
699 blockOutgoing.name = @"outgoing-queue-hold";
700 CKKSResultOperation* outgoingQueueOperation = [self.keychainView processOutgoingQueueAfter:blockOutgoing ckoperationGroup:nil];
702 CKKSResultOperation* blockIncoming = [CKKSResultOperation operationWithBlock:^{
703 secnotice("ckks", "Incoming queue hold released.");
705 blockIncoming.name = @"incoming-queue-hold";
706 CKKSResultOperation* incomingQueueOperation = [self.keychainView processIncomingQueue:false after: blockIncoming];
708 [self addGenericPassword:@"localchange" account:@"account-delete-me"];
710 // Pull out the new item's UUID.
711 __block NSString* itemUUID = nil;
712 [self.keychainView dispatchSync:^bool {
713 NSError* error = nil;
714 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID ?: [[CKRecordZoneID alloc] initWithZoneName:@"keychain"
715 ownerName:CKCurrentUserDefaultName]
717 XCTAssertNil(error, "no error fetching uuids");
718 XCTAssertEqual(uuids.count, 1u, "There's exactly one outgoing queue entry");
721 XCTAssertNotNil(itemUUID, "Have a UUID for our new item");
725 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName: itemUUID]];
727 [self.keychainView notifyZoneChange:nil];
728 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
730 // Allow the outgoing queue operation to proceed
731 [self.operationQueue addOperation:blockOutgoing];
732 [outgoingQueueOperation waitUntilFinished];
734 // Allow the incoming queue operation to proceed
735 [self.operationQueue addOperation:blockIncoming];
736 [incomingQueueOperation waitUntilFinished];
738 [self checkGenericPassword:@"data" account:@"account-delete-me"];
740 [self.keychainView waitUntilAllOperationsAreFinished];
743 - (void)testReceiveCloudKitConflictOnJustAddedItems {
744 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
745 [self startCKKSSubsystem];
747 [self.keychainView waitForKeyHierarchyReadiness];
748 [self.keychainView waitUntilAllOperationsAreFinished];
750 // Place a hold on processing the outgoing queue.
751 self.keychainView.holdOutgoingQueueOperation = [CKKSResultOperation named:@"outgoing-queue-hold" withBlock:^{
752 secnotice("ckks", "Outgoing queue hold released.");
755 [self addGenericPassword:@"localchange" account:@"account-delete-me"];
757 // Pull out the new item's UUID.
758 __block NSString* itemUUID = nil;
759 [self.keychainView dispatchSync:^bool {
760 NSError* error = nil;
761 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID ?: [[CKRecordZoneID alloc] initWithZoneName:@"keychain"
762 ownerName:CKCurrentUserDefaultName]
764 XCTAssertNil(error, "no error fetching uuids");
765 XCTAssertEqual(uuids.count, 1u, "There's exactly one outgoing queue entry");
768 XCTAssertNotNil(itemUUID, "Have a UUID for our new item");
772 // Add a second item: this item should be uploaded after the failure of the first item
773 [self addGenericPassword:@"localchange" account:@"account-delete-me-2"];
775 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName: itemUUID]];
777 // Also, this write will increment the class C current pointer's etag
778 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
779 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
780 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
781 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
782 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
784 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
785 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
786 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
788 // Allow the outgoing queue operation to proceed
789 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
791 OCMVerifyAllWithDelay(self.mockDatabase, 20);
792 [self.keychainView waitUntilAllOperationsAreFinished];
794 [self checkGenericPassword:@"data" account:@"account-delete-me"];
795 [self checkGenericPassword:@"localchange" account:@"account-delete-me-2"];
799 -(void)testReceiveUnknownField {
800 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
802 [self startCKKSSubsystem];
803 [self.keychainView waitForKeyHierarchyReadiness];
805 NSError* error = nil;
807 // Manually encrypt an item
808 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
809 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
810 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
811 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
812 parentKeyUUID:self.keychainZoneKeys.classA.uuid
813 zoneID:recordID.zoneID];
814 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classA error:&error];
815 XCTAssertNotNil(itemkey, "Got a key");
816 cipheritem.wrappedkey = itemkey.wrappedkey;
817 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
819 NSData* future_data_field = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding];
820 NSString* future_string_field = @"authstring";
821 NSString* future_server_field = @"server_can_change_at_any_time";
822 NSNumber* future_number_field = [NSNumber numberWithInt:30];
824 // Use version 2, so future fields will be authenticated
825 cipheritem.encver = CKKSItemEncryptionVersion2;
826 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
828 authenticatedData[@"future_data_field"] = future_data_field;
829 authenticatedData[@"future_string_field"] = [future_string_field dataUsingEncoding:NSUTF8StringEncoding];
831 uint64_t n = OSSwapHostToLittleConstInt64([future_number_field unsignedLongValue]);
832 authenticatedData[@"future_number_field"] = [NSData dataWithBytes:&n length:sizeof(n)];
835 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
836 XCTAssertNil(error, "no error encrypting object");
837 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
839 CKRecord* ckr = [cipheritem CKRecordWithZoneID: recordID.zoneID];
840 ckr[@"future_data_field"] = future_data_field;
841 ckr[@"future_string_field"] = future_string_field;
842 ckr[@"future_number_field"] = future_number_field;
843 ckr[@"server_new_server_field"] = future_server_field;
844 [self.keychainZone addToZone:ckr];
846 [self.keychainView waitForFetchAndIncomingQueueProcessing];
848 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
849 (id)kSecReturnAttributes: @YES,
850 (id)kSecAttrSynchronizable: @YES,
851 (id)kSecAttrAccount: @"account-delete-me",
852 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
854 CFTypeRef cfresult = NULL;
855 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
857 // Test that if this item is updated, it remains encrypted in v2, and future_field still exists
858 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
859 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
861 OCMVerifyAllWithDelay(self.mockDatabase, 20);
862 [self waitForCKModifications];
864 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
865 XCTAssertEqualObjects(newRecord[@"future_data_field"], future_data_field, "future_data_field still exists");
866 XCTAssertEqualObjects(newRecord[@"future_string_field"], future_string_field, "future_string_field still exists");
867 XCTAssertEqualObjects(newRecord[@"future_number_field"], future_number_field, "future_string_field still exists");
868 XCTAssertEqualObjects(newRecord[@"server_new_server_field"], future_server_field, "future_server_field stille exists");
870 CKKSItem* newItem = [[CKKSItem alloc] initWithCKRecord:newRecord];
871 CKKSAESSIVKey* newItemKey = [self.keychainZoneKeys.classA unwrapAESKey:newItem.wrappedkey error:&error];
872 XCTAssertNil(error, "No error unwrapping AES key");
873 XCTAssertNotNil(newItemKey, "Have an unwrapped AES key for this item");
875 NSDictionary* uploadedData = [CKKSItemEncrypter decryptDictionary:newRecord[SecCKRecordDataKey]
877 authenticatedData:authenticatedData
879 XCTAssertNil(error, "No error decrypting dictionary");
880 XCTAssertNotNil(uploadedData, "Authenticated re-uploaded data including future_field");
881 XCTAssertEqualObjects(uploadedData[@"v_Data"], [@"different password" dataUsingEncoding:NSUTF8StringEncoding], "Passwords match");
885 -(void)testReceiveRecordEncryptedv1 {
886 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
888 [self startCKKSSubsystem];
889 [self.keychainView waitForKeyHierarchyReadiness];
891 NSError* error = nil;
893 // Manually encrypt an item
894 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
895 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
896 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
897 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
898 parentKeyUUID:self.keychainZoneKeys.classC.uuid
899 zoneID:recordID.zoneID];
900 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
901 XCTAssertNotNil(itemkey, "Got a key");
902 cipheritem.wrappedkey = itemkey.wrappedkey;
903 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
905 cipheritem.encver = CKKSItemEncryptionVersion1;
907 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:cipheritem.encver] mutableCopy];
909 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
910 XCTAssertNil(error, "no error encrypting object");
911 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
913 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
915 [self.keychainView waitForFetchAndIncomingQueueProcessing];
917 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
918 (id)kSecReturnAttributes: @YES,
919 (id)kSecAttrSynchronizable: @YES,
920 (id)kSecAttrAccount: @"account-delete-me",
921 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
923 CFTypeRef cfresult = NULL;
924 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
925 CFReleaseNull(cfresult);
927 // Test that if this item is updated, it is encrypted in v2
928 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
929 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
931 OCMVerifyAllWithDelay(self.mockDatabase, 20);
932 [self waitForCKModifications];
934 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
935 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
938 - (void)testUploadPagination {
939 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
941 for(size_t count = 0; count < 250; count++) {
942 [self addGenericPassword: @"data" account: [NSString stringWithFormat:@"account-delete-me-%03lu", count]];
945 [self startCKKSSubsystem];
947 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
948 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
949 [self expectCKModifyItemRecords: 50 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
951 OCMVerifyAllWithDelay(self.mockDatabase, 20);
954 - (void)testUploadInitialKeyHierarchy {
955 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
956 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
958 // Spin up CKKS subsystem.
959 [self startCKKSSubsystem];
961 OCMVerifyAllWithDelay(self.mockDatabase, 20);
964 - (void)testUploadInitialKeyHierarchyAfterLockedStart {
966 self.aksLockState = true;
967 [self.lockStateTracker recheck];
969 [self startCKKSSubsystem];
971 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
972 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitforunlock");
974 // After unlock, the key hierarchy should be created.
975 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
977 self.aksLockState = false;
978 [self.lockStateTracker recheck];
980 OCMVerifyAllWithDelay(self.mockDatabase, 20);
982 // We expect a single class C record to be uploaded.
983 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
985 [self addGenericPassword: @"data" account: @"account-delete-me"];
986 OCMVerifyAllWithDelay(self.mockDatabase, 20);
989 - (void)testLockImmediatelyAfterUploadingInitialKeyHierarchy {
991 // Upon upload, block fetches
992 __weak __typeof(self) weakSelf = self;
993 [self expectCKModifyRecords: @{
994 SecCKRecordIntermediateKeyType: [NSNumber numberWithUnsignedInteger: 3],
995 SecCKRecordCurrentKeyType: [NSNumber numberWithUnsignedInteger: 3],
996 SecCKRecordTLKShareType: [NSNumber numberWithUnsignedInteger: 1],
998 deletedRecordTypeCounts:nil
999 zoneID:self.keychainZoneID
1000 checkModifiedRecord:nil
1001 runAfterModification:^{
1002 __strong __typeof(self) strongSelf = weakSelf;
1003 [strongSelf holdCloudKitFetches];
1006 [self startCKKSSubsystem];
1008 // Should enter 'ready'
1009 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1010 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1012 // Now, lock and allow fetches again
1013 self.aksLockState = true;
1014 [self.lockStateTracker recheck];
1015 [self releaseCloudKitFetchHold];
1017 CKKSResultOperation* op = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
1018 [op waitUntilFinished];
1020 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1022 // Wait for CKKS to shake itself out...
1023 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
1025 // Should be in ReadyPendingUnlock
1026 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1028 // We expect a single class C record to be uploaded.
1029 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1030 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1032 [self addGenericPassword: @"data" account: @"account-delete-me"];
1033 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1036 - (void)testReceiveKeyHierarchyAfterLockedStart {
1037 // 'Lock' the keybag
1038 self.aksLockState = true;
1039 [self.lockStateTracker recheck];
1041 [self startCKKSSubsystem];
1043 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1044 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateFetchComplete] wait:20*NSEC_PER_SEC], @"Key state should get stuck in fetchcomplete");
1046 // Now, another device comes along and creates the hierarchy; we download it; and it and sends us the TLK
1047 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1048 [self.keychainView notifyZoneChange:nil];
1049 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
1051 self.aksLockState = false;
1052 [self.lockStateTracker recheck];
1054 // After unlock, the TLK arrives
1055 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1056 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1058 // We expect a single class C record to be uploaded.
1059 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1061 [self addGenericPassword: @"data" account: @"account-delete-me"];
1062 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1065 - (void)testLoadKeyHierarchyAfterLockedStart {
1066 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
1068 // 'Lock' the keybag
1069 self.aksLockState = true;
1070 [self.lockStateTracker recheck];
1072 [self startCKKSSubsystem];
1074 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1075 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1077 self.aksLockState = false;
1078 [self.lockStateTracker recheck];
1080 // We expect a single class C record to be uploaded.
1081 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1083 [self addGenericPassword: @"data" account: @"account-delete-me"];
1084 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1087 - (void)testUploadAndUseKeyHierarchy {
1088 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
1089 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1091 [self startCKKSSubsystem];
1093 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
1094 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1095 (id)kSecAttrAccount : @"account-delete-me",
1096 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1097 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1099 CFTypeRef item = NULL;
1100 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not exist");
1102 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1103 [self waitForCKModifications];
1105 // We expect a single class C record to be uploaded.
1106 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1108 [self addGenericPassword: @"data" account: @"account-delete-me"];
1109 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1111 // now, expect a single class A record to be uploaded
1112 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1114 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef)@{
1115 (id)kSecClass : (id)kSecClassGenericPassword,
1116 (id)kSecAttrAccessGroup : @"com.apple.security.sos",
1117 (id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked,
1118 (id)kSecAttrAccount : @"account-class-A",
1119 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1120 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
1121 }, NULL), @"Adding class A item");
1122 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1125 - (void)testUploadInitialKeyHierarchyTriggersBackup {
1126 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
1127 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1129 // We also expect the view manager's notifyNewTLKsInKeychain call to fire (after some delay)
1130 OCMExpect([self.mockCKKSViewManager notifyNewTLKsInKeychain]);
1132 // Spin up CKKS subsystem.
1133 [self startCKKSSubsystem];
1135 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1136 OCMVerifyAllWithDelay(self.mockCKKSViewManager, 10);
1139 - (void)testResetCloudKitZoneFromNoTLK {
1140 self.silentZoneDeletesAllowed = true;
1142 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1143 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1144 // explicitly do not save a fake device status here
1145 self.keychainZone.flag = true;
1147 // It'll eventually upload a new key hierarchy
1148 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
1150 [self startCKKSSubsystem];
1151 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1153 // But then, it'll fire off the reset and reach 'ready'
1154 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1155 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1157 // And the zone should have been cleared and re-made
1158 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1161 - (void)testResetCloudKitZoneFromNoTLKWithOtherWaitForTLKDevices {
1162 self.silentZoneDeletesAllowed = true;
1164 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1165 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1166 // Save a fake device status here, but modify its key state to be 'waitfortlk': it has no idea what the TLK is either
1167 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1169 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1170 if([record.recordType isEqualToString:SecCKRecordDeviceStateType]) {
1171 record[SecCKRecordKeyState] = CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTLK);
1175 self.keychainZone.flag = true;
1177 // It'll eventually upload a new key hierarchy
1178 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
1180 [self startCKKSSubsystem];
1181 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1183 // But then, it'll fire off the reset and reach 'ready'
1184 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1185 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1187 // And the zone should have been cleared and re-made
1188 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1191 - (void)testResetCloudKitZoneFromNoTLKIgnoringInactiveDevices {
1192 self.silentZoneDeletesAllowed = true;
1194 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1195 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1196 // Save a fake device status here, but modify its creation and modification times to be months ago
1197 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1199 // Put a 'in-circle' TLKShare record, but also modify its creation and modification times
1200 CKKSSOSSelfPeer* untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"untrusted-peer"
1201 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1202 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
1203 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:untrustedPeer to:untrustedPeer zoneID:self.keychainZoneID];
1205 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1206 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1207 record.creationDate = [NSDate distantPast];
1208 record.modificationDate = [NSDate distantPast];
1212 self.keychainZone.flag = true;
1214 // It'll eventually upload a new key hierarchy
1215 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
1217 [self startCKKSSubsystem];
1218 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1220 // But then, it'll fire off the reset and reach 'ready'
1221 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1222 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1224 // And the zone should have been cleared and re-made
1225 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1228 - (void)testDoNotResetCloudKitZoneFromWaitForTLKDueToRecentDeviceState {
1229 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1231 // CKKS shouldn't reset this zone, due to a recent device status claiming to have TLKs
1232 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1234 NSDateComponents* offset = [[NSDateComponents alloc] init];
1236 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1237 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1238 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1239 record.creationDate = updateTime;
1240 record.modificationDate = updateTime;
1244 self.keychainZone.flag = true;
1245 [self startCKKSSubsystem];
1247 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1249 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1252 - (void)testDoNotCloudKitZoneFromWaitForTLKDueToRecentButUntrustedDeviceState {
1253 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1255 // CKKS should reset this zone, even though to a recent device status claiming to have TLKs. The device isn't trusted
1256 self.silentZoneDeletesAllowed = true;
1257 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1258 [self.currentPeers removeObject:self.remoteSOSOnlyPeer];
1260 self.keychainZone.flag = true;
1261 [self startCKKSSubsystem];
1263 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1264 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1266 // And ensure it doesn't go on to 'reset'
1267 XCTAssertNotEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:100*NSEC_PER_MSEC], @"Key state should not become 'resetzone'");
1270 - (void)testResetCloudKitZoneFromWaitForTLKDueToLessRecentAndUntrustedDeviceState {
1271 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1273 // CKKS should reset this zone, even though to a recent device status claiming to have TLKs. The device isn't trusted
1274 self.silentZoneDeletesAllowed = true;
1275 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1276 [self.currentPeers removeObject:self.remoteSOSOnlyPeer];
1278 NSDateComponents* offset = [[NSDateComponents alloc] init];
1280 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1281 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1282 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1283 record.creationDate = updateTime;
1284 record.modificationDate = updateTime;
1288 self.keychainZone.flag = true;
1289 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
1290 [self startCKKSSubsystem];
1291 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1293 // Then we should reset.
1294 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1295 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1297 // And the zone should have been cleared and re-made
1298 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1301 - (void)testAcceptExistingKeyHierarchy {
1302 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
1303 // Test also begins with the TLK having arrived in the local keychain (via SOS)
1304 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1305 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1306 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1308 // Spin up CKKS subsystem.
1309 [self startCKKSSubsystem];
1311 // The CKKS subsystem should only upload its TLK share
1312 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1314 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1316 // Verify that there are three local keys, and three local current key records
1317 __weak __typeof(self) weakSelf = self;
1318 [self.keychainView dispatchSync: ^bool{
1319 __strong __typeof(weakSelf) strongSelf = weakSelf;
1320 XCTAssertNotNil(strongSelf, "self exists");
1322 NSError* error = nil;
1324 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
1325 XCTAssertNil(error, "no error fetching keys");
1326 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
1328 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1329 XCTAssertNil(error, "no error fetching current keys");
1330 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1336 - (void)testAcceptExistingAndUseKeyHierarchy {
1337 // Test starts with nothing in database, but one in our fake CloudKit.
1338 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1339 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1340 // But, CKKS shouldn't ever reset the zone
1341 self.keychainZone.flag = true;
1343 [self startCKKSSubsystem];
1344 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should have become waitfortlk");
1346 // Now, save the TLK to the keychain (to simulate it coming in later via SOS). We'll create a TLK share for ourselves.
1347 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1348 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1350 // Wait for the key hierarchy to sort itself out, to make it easier on this test; see testOnboardOldItemsWithExistingKeyHierarchy for the other test.
1351 // The CKKS subsystem should write its TLK share, but nothing else
1352 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1354 // We expect a single record to be uploaded for each key class
1355 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1356 [self addGenericPassword: @"data" account: @"account-delete-me"];
1357 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1359 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1360 [self addGenericPassword:@"asdf"
1361 account:@"account-class-A"
1363 access:(id)kSecAttrAccessibleWhenUnlocked
1364 expecting:errSecSuccess
1365 message:@"Adding class A item"];
1366 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1367 XCTAssertTrue(self.keychainZone.flag, "Keychain zone shouldn't have been reset");
1370 - (void)testAcceptExistingKeyHierarchyDespiteLocked {
1371 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
1372 // Test also begins with the TLK having arrived in the local keychain (via SOS)
1374 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1375 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1377 self.aksLockState = true;
1378 [self.lockStateTracker recheck];
1380 // Spin up CKKS subsystem.
1381 [self startCKKSSubsystem];
1383 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:20*NSEC_PER_SEC], "Key state should have become waitforunlock");
1385 // CKKS will give itself a TLK Share
1386 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1388 // Now that all operations are complete, 'unlock' AKS
1389 self.aksLockState = false;
1390 [self.lockStateTracker recheck];
1392 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1393 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1395 // Verify that there are three local keys, and three local current key records
1396 __weak __typeof(self) weakSelf = self;
1397 [self.keychainView dispatchSync: ^bool{
1398 __strong __typeof(weakSelf) strongSelf = weakSelf;
1399 XCTAssertNotNil(strongSelf, "self exists");
1401 NSError* error = nil;
1403 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
1404 XCTAssertNil(error, "no error fetching keys");
1405 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
1407 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1408 XCTAssertNil(error, "no error fetching current keys");
1409 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1415 - (void)testReceiveClassCWhileALocked {
1416 // Test starts with a key hierarchy already existing.
1417 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
1418 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1419 [self startCKKSSubsystem];
1421 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1422 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1424 [self findGenericPassword:@"classCItem" expecting:errSecItemNotFound];
1425 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
1427 // 'Lock' the keybag
1428 self.aksLockState = true;
1429 [self.lockStateTracker recheck];
1431 XCTAssertNotNil(self.keychainZoneKeys, "Have zone keys for zone");
1432 XCTAssertNotNil(self.keychainZoneKeys.classA, "Have class A key for zone");
1433 XCTAssertNotNil(self.keychainZoneKeys.classC, "Have class C key for zone");
1435 [self.keychainView dispatchSyncWithAccountKeys: ^bool {
1436 [self.keychainView _onqueueKeyStateMachineRequestProcess];
1439 // And ensure we end up back in 'readypendingunlock': we have the keys, we're just locked now
1440 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
1441 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1443 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"classCItem" key:self.keychainZoneKeys.classC]];
1444 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-FFFF-FFFF-FFFF-5A507ACB2D85" withAccount:@"classAItem" key:self.keychainZoneKeys.classA]];
1446 CKKSResultOperation* op = [self.keychainView waitForFetchAndIncomingQueueProcessing];
1447 // The processing op should NOT error, even though it didn't manage to process the classA item
1448 XCTAssertNil(op.error, "no error while failing to process a class A item");
1450 CKKSResultOperation* erroringOp = [self.keychainView processIncomingQueue:true];
1451 [erroringOp waitUntilFinished];
1452 XCTAssertNotNil(erroringOp.error, "error exists while processing a class A item");
1454 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
1455 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
1457 self.aksLockState = false;
1458 [self.lockStateTracker recheck];
1459 [self.keychainView waitUntilAllOperationsAreFinished];
1461 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
1462 [self findGenericPassword:@"classAItem" expecting:errSecSuccess];
1465 - (void)testRestartWhileLocked {
1466 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1467 [self startCKKSSubsystem];
1469 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1471 // 'Lock' the keybag
1472 self.aksLockState = true;
1473 [self.lockStateTracker recheck];
1475 [self.keychainView halt];
1476 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1478 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1480 self.aksLockState = false;
1481 [self.lockStateTracker recheck];
1483 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1486 - (void)testExternalKeyRoll {
1487 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1488 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1489 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1490 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1492 // Spin up CKKS subsystem.
1493 [self startCKKSSubsystem];
1495 // The CKKS subsystem should not try to write anything to the CloudKit database.
1496 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1498 __weak __typeof(self) weakSelf = self;
1500 // We expect a single record to be uploaded.
1501 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1503 [self addGenericPassword: @"data" account: @"account-delete-me"];
1505 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1506 [self waitForCKModifications];
1508 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1509 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1510 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1512 // Trigger a notification
1513 [self.keychainView notifyZoneChange:nil];
1515 // Make life easy on this test; testAcceptKeyConflictAndUploadReencryptedItem will check the case when we don't receive the notification
1516 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1518 // Just in extra case of threading issues, force a reexamination of the key hierarchy
1519 [self.keychainView dispatchSyncWithAccountKeys: ^bool {
1520 [self.keychainView _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
1524 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1526 // Verify that there are six local keys, and three local current key records
1527 [self.keychainView dispatchSync: ^bool{
1528 __strong __typeof(weakSelf) strongSelf = weakSelf;
1529 XCTAssertNotNil(strongSelf, "self exists");
1531 NSError* error = nil;
1532 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:self.keychainZoneID error:&error];
1533 XCTAssertNil(error, "no error fetching keys");
1534 XCTAssertEqual(keys.count, 6u, "Six keys in local database");
1536 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1537 XCTAssertNil(error, "no error fetching current keys");
1538 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1540 for(CKKSCurrentKeyPointer* key in currentkeys) {
1541 if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
1542 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.tlk.uuid);
1543 } else if([key.keyclass isEqualToString: SecCKKSKeyClassA]) {
1544 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classA.uuid);
1545 } else if([key.keyclass isEqualToString: SecCKKSKeyClassC]) {
1546 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classC.uuid);
1548 XCTFail("Unknown key class: %@", key.keyclass);
1555 // We expect a single record to be uploaded.
1556 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1558 // TODO: remove this by writing code for item reencrypt after key arrival
1559 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1561 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1563 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1566 - (void)testAcceptKeyConflictAndUploadReencryptedItem {
1567 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1568 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1569 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1570 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1572 [self startCKKSSubsystem];
1573 [self.keychainView waitUntilAllOperationsAreFinished];
1575 // We expect a single record to be uploaded.
1576 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1578 [self addGenericPassword: @"data" account: @"account-delete-me"];
1580 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1581 [self waitForCKModifications];
1583 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1585 // Do not trigger a notification here. This should cause a conflict updating the current key records
1587 // We expect a single record to be uploaded, but that the write will be rejected
1588 // We then expect that item to be reuploaded with the current key
1590 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
1591 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1592 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1594 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under rolled class C key in hierarchy"]];
1596 // New key arrives via SOS!
1597 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1598 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1600 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1603 - (void)testAcceptKeyConflictAndUploadReencryptedItems {
1604 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1605 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1606 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1607 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1609 [self startCKKSSubsystem];
1610 [self.keychainView waitUntilAllOperationsAreFinished];
1612 // We expect a single record to be uploaded.
1613 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1614 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1616 [self addGenericPassword: @"data" account: @"account-delete-me"];
1618 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1619 [self waitForCKModifications];
1621 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1623 // Do not trigger a notification here. This should cause a conflict updating the current key records
1625 // We expect a single record to be uploaded, but that the write will be rejected
1626 // We then expect that item to be reuploaded with the current key
1628 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
1629 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1630 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key-2"];
1631 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1633 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1634 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under rolled class C key in hierarchy"]];
1636 // New key arrives via SOS!
1637 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1638 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1640 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1643 - (void)testRecoverFromRequestKeyRefetchWithoutRolling {
1644 // Simply requesting a key state refetch shouldn't roll the key hierarchy.
1646 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1648 // Spin up CKKS subsystem.
1649 [self startCKKSSubsystem];
1651 // Items should upload.
1652 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1653 [self addGenericPassword: @"data" account: @"account-delete-me"];
1654 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1656 [self waitForCKModifications];
1659 // CKKS should not roll the keys while progressing back to 'ready', but it will fetch once
1660 self.silentFetchesAllowed = false;
1661 [self expectCKFetch];
1663 [self.keychainView dispatchSyncWithAccountKeys: ^bool {
1664 [self.keychainView _onqueueKeyStateMachineRequestFetch];
1668 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
1669 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1672 - (void)testRecoverFromIncrementedCurrentKeyPointerEtag {
1673 // CloudKit sometimes reports the current key pointers have changed (etag mismatch), but their content hasn't.
1674 // In this case, CKKS shouldn't roll the TLK.
1676 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1678 // Spin up CKKS subsystem.
1679 [self startCKKSSubsystem];
1680 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // just to be sure it's fetched
1682 // Items should upload.
1683 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1684 [self addGenericPassword: @"data" account: @"account-delete-me"];
1685 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1687 [self waitForCKModifications];
1689 // Bump the etag on the class C current key record, but don't change any data
1690 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
1691 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
1692 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
1694 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
1695 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
1697 // Add another item. This write should fail, then CKKS should recover without rolling the key hierarchy or issuing a fetch.
1698 self.silentFetchesAllowed = false;
1699 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1700 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1701 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
1702 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1705 - (void)testRecoverMultipleItemsFromIncrementedCurrentKeyPointerEtag {
1706 // CloudKit sometimes reports the current key pointers have changed (etag mismatch), but their content hasn't.
1707 // In this case, CKKS shouldn't roll the TLK.
1708 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1710 // Spin up CKKS subsystem.
1711 [self startCKKSSubsystem];
1712 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // just to be sure it's fetched
1714 // Items should upload.
1715 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1716 [self addGenericPassword: @"data" account: @"account-delete-me"];
1717 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1719 [self waitForCKModifications];
1721 // Bump the etag on the class C current key record, but don't change any data
1722 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
1723 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
1724 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
1726 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
1727 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
1729 // Add another item. This write should fail, then CKKS should recover without rolling the key hierarchy or issuing a fetch.
1730 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
1731 secnotice("ckks", "releasing outgoing-queue hold");
1734 self.silentFetchesAllowed = false;
1735 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1736 [self expectCKModifyItemRecords:2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1737 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
1738 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
1740 [self.operationQueue addOperation: self.keychainView.holdOutgoingQueueOperation];
1741 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1744 - (void)testOnboardOldItemsCreatingKeyHierarchy {
1745 // 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
1747 // Test starts with nothing in CloudKit, and CKKS blocked. Add one item without a UUID...
1749 SecCKKSTestSetDisableAutomaticUUID(true);
1750 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1752 // and an item with a UUID...
1753 SecCKKSTestSetDisableAutomaticUUID(false);
1754 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1756 // We expect an upload of the key hierarchy
1757 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1759 // We then expect an upload of the added items
1760 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1762 [self startCKKSSubsystem];
1764 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1767 - (void)testOnboardOldItemsWithExistingKeyHierarchy {
1768 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1770 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1771 [self addGenericPassword: @"data" account: @"account-delete-me"];
1773 [self startCKKSSubsystem];
1774 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1777 - (void)testOnboardOldItemsWithExistingKeyHierarchyExtantTLK {
1778 // Test starts key hierarchy in our fake CloudKit, the TLK arrived in the local keychain, and CKKS blocked.
1779 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1780 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1781 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1783 // Add one item without a UUID...
1784 SecCKKSTestSetDisableAutomaticUUID(true);
1785 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1787 // and an item with a UUID...
1788 SecCKKSTestSetDisableAutomaticUUID(false);
1789 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1791 // 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
1792 // We expect a single record to be uploaded.
1793 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1795 // Spin up CKKS subsystem.
1796 [self startCKKSSubsystem];
1798 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1801 - (void)testOnboardOldItemsWithExistingKeyHierarchyLateTLK {
1802 // Test starts key hierarchy in our fake CloudKit, and CKKS blocked.
1803 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1804 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1805 self.keychainZone.flag = true;
1807 // Add one item without a UUID...
1808 SecCKKSTestSetDisableAutomaticUUID(true);
1809 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1811 // and an item with a UUID...
1812 SecCKKSTestSetDisableAutomaticUUID(false);
1813 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1815 // 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
1817 // Spin up CKKS subsystem.
1818 [self startCKKSSubsystem];
1819 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should have become waitfortlk");
1821 // Now, save the TLK to the keychain (to simulate it coming in via SOS).
1822 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1823 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1825 // We expect a single record to be uploaded.
1826 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1828 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1829 XCTAssertTrue(self.keychainZone.flag, "Keychain zone shouldn't have been reset");
1832 - (void)testResync {
1833 // We need to set up a desynced situation to test our resync.
1834 // First, let CKKS start up and send several items to CloudKit (that we'll then desync!)
1835 __block NSError* error = nil;
1837 // Test starts with keys in CloudKit (so we can create items later)
1838 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1839 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1840 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1842 [self addGenericPassword: @"data" account: @"first"];
1843 [self addGenericPassword: @"data" account: @"second"];
1844 [self addGenericPassword: @"data" account: @"third"];
1845 [self addGenericPassword: @"data" account: @"fourth"];
1846 NSUInteger passwordCount = 4u;
1848 [self checkGenericPassword: @"data" account: @"first"];
1849 [self checkGenericPassword: @"data" account: @"second"];
1850 [self checkGenericPassword: @"data" account: @"third"];
1851 [self checkGenericPassword: @"data" account: @"fourth"];
1853 [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1855 [self startCKKSSubsystem];
1857 // Wait for uploads to happen
1858 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1859 [self waitForCKModifications];
1860 // One TLK share record
1861 XCTAssertEqual(self.keychainZone.currentDatabase.count, SYSTEM_DB_RECORD_COUNT+passwordCount+1, "Have SYSTEM_DB_RECORD_COUNT+passwordCount+1 objects in cloudkit");
1863 // Now, corrupt away!
1864 // Extract all passwordCount items for Corruption
1865 NSArray<CKRecord*>* items = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
1866 XCTAssertEqual(items.count, passwordCount, "Have %lu Items in cloudkit", (unsigned long)passwordCount);
1868 // For the first record, delete all traces of it from CKKS. But! it remains in local keychain.
1869 // Expected outcome: CKKS resyncs; item exists again.
1870 CKRecord* delete = items[0];
1871 NSString* deleteAccount = [[self decryptRecord: delete] objectForKey: (__bridge id) kSecAttrAccount];
1872 XCTAssertNotNil(deleteAccount, "received an account for the local delete object");
1874 __weak __typeof(self) weakSelf = self;
1875 [self.keychainView dispatchSync:^bool{
1876 __strong __typeof(weakSelf) strongSelf = weakSelf;
1877 XCTAssertNotNil(strongSelf, "self exists");
1879 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1881 [ckme deleteFromDatabase: &error];
1883 XCTAssertNil(error, "no error removing CKME");
1884 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1886 [oqe deleteFromDatabase: &error];
1888 XCTAssertNil(error, "no error removing OQE");
1889 CKKSIncomingQueueEntry* iqe = [CKKSIncomingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1891 [iqe deleteFromDatabase: &error];
1893 XCTAssertNil(error, "no error removing IQE");
1897 // For the second record, delete all traces of it from CloudKit.
1898 // Expected outcome: deleted locally
1899 CKRecord* remoteDelete = items[1];
1900 NSString* remoteDeleteAccount = [[self decryptRecord: remoteDelete] objectForKey: (__bridge id) kSecAttrAccount];
1901 XCTAssertNotNil(remoteDeleteAccount, "received an account for the remote delete object");
1903 [self.keychainZone deleteCKRecordIDFromZone: remoteDelete.recordID];
1904 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
1905 [database removeObjectForKey: remoteDelete.recordID];
1908 // The third record gets modified in CloudKit, but not locally.
1909 // Expected outcome: use the CloudKit version
1910 CKRecord* remoteDataChanged = items[2];
1911 NSMutableDictionary* remoteDataDictionary = [[self decryptRecord: remoteDataChanged] mutableCopy];
1912 NSString* remoteDataChangedAccount = [remoteDataDictionary objectForKey: (__bridge id) kSecAttrAccount];
1913 XCTAssertNotNil(remoteDataChangedAccount, "Received an account for the remote-data-changed object");
1914 remoteDataDictionary[(__bridge id) kSecValueData] = [@"CloudKitWins" dataUsingEncoding: NSUTF8StringEncoding];
1916 CKRecord* newData = [self newRecord: remoteDataChanged.recordID withNewItemData: remoteDataDictionary];
1917 [self.keychainZone addToZone: newData];
1918 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
1919 database[remoteDataChanged.recordID] = newData;
1922 // The fourth record stays in-sync. Good work, everyone!
1923 // Expected outcome: stays in-sync
1924 NSString* insyncAccount = [[self decryptRecord: items[3]] objectForKey: (__bridge id) kSecAttrAccount];
1925 XCTAssertNotNil(insyncAccount, "Received an account for the in-sync object");
1927 // The fifth record gets magically added to CloudKit, but CKKS has never heard of it
1928 // (emulates a lost record on the client, but that CloudKit already believes it's sent the record for)
1929 // Expected outcome: added to local keychain
1930 NSString* remoteOnlyAccount = @"remote-only";
1931 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount: remoteOnlyAccount];
1932 [self.keychainZone addToZone: ckr];
1933 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
1934 database[ckr.recordID] = ckr;
1937 ckksnotice("ckksresync", self.keychainView, "local delete: %@ %@", delete.recordID.recordName, deleteAccount);
1938 ckksnotice("ckksresync", self.keychainView, "Remote deletion: %@ %@", remoteDelete.recordID.recordName, remoteDeleteAccount);
1939 ckksnotice("ckksresync", self.keychainView, "Remote data changed: %@ %@", remoteDataChanged.recordID.recordName, remoteDataChangedAccount);
1940 ckksnotice("ckksresync", self.keychainView, "in-sync: %@ %@", items[3].recordID.recordName, insyncAccount);
1941 ckksnotice("ckksresync", self.keychainView, "Remote only: %@ %@", ckr.recordID.recordName, remoteOnlyAccount);
1943 CKKSSynchronizeOperation* resyncOperation = [self.keychainView resyncWithCloud];
1944 [resyncOperation waitUntilFinished];
1946 XCTAssertNil(resyncOperation.error, "No error during the resync operation");
1948 // Now do some checking. Remember, we don't know which record we corrupted, so use the parsed account variables to check.
1950 [self findGenericPassword: deleteAccount expecting: errSecSuccess];
1951 [self findGenericPassword: remoteDeleteAccount expecting: errSecItemNotFound];
1952 [self findGenericPassword: remoteDataChangedAccount expecting: errSecSuccess];
1953 [self findGenericPassword: insyncAccount expecting: errSecSuccess];
1954 [self findGenericPassword: remoteOnlyAccount expecting: errSecSuccess];
1956 [self checkGenericPassword: @"data" account: deleteAccount];
1957 //[self checkGenericPassword: @"data" account: remoteDeleteAccount];
1958 [self checkGenericPassword: @"CloudKitWins" account: remoteDataChangedAccount];
1959 [self checkGenericPassword: @"data" account: insyncAccount];
1960 [self checkGenericPassword: @"data" account: remoteOnlyAccount];
1962 [self.keychainView dispatchSync:^bool{
1963 __strong __typeof(weakSelf) strongSelf = weakSelf;
1964 XCTAssertNotNil(strongSelf, "self exists");
1966 CKKSMirrorEntry* ckme = nil;
1968 ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1969 XCTAssertNil(error);
1970 XCTAssertNotNil(ckme);
1972 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDelete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1973 XCTAssertNil(error);
1974 XCTAssertNil(ckme); // deleted!
1976 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDataChanged.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1977 XCTAssertNil(error);
1978 XCTAssertNotNil(ckme);
1980 ckme = [CKKSMirrorEntry tryFromDatabase:items[3].recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1981 XCTAssertNil(error);
1982 XCTAssertNotNil(ckme);
1984 ckme = [CKKSMirrorEntry tryFromDatabase:ckr.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1985 XCTAssertNil(error);
1986 XCTAssertNotNil(ckme);
1991 - (void)testResyncItemsMissingFromLocalKeychain {
1992 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1995 // one password correctly synced between local keychain and CloudKit
1996 // one password incorrectly disappeared from local keychain, but in mirror table
1997 // one password sitting in the outgoing queue
1998 // one password sitting in the incoming queue
2000 // Add and sync two passwords
2001 [self addGenericPassword: @"data" account: @"first"];
2002 [self addGenericPassword: @"data" account: @"second"];
2004 [self checkGenericPassword: @"data" account: @"first"];
2005 [self checkGenericPassword: @"data" account: @"second"];
2007 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2008 [self startCKKSSubsystem];
2009 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2010 [self waitForCKModifications];
2011 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2013 // Now, place an item in the outgoing queue
2015 //[self addGenericPassword: @"data" account: @"third"];
2016 //[self checkGenericPassword: @"data" account: @"third"];
2018 // Now, corrupt away!
2019 // Extract all passwordCount items for Corruption
2020 NSArray<CKRecord*>* items = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
2021 XCTAssertEqual(items.count, 2u, "Have %lu Items in cloudkit", (unsigned long)2u);
2023 // For the first record, surreptitiously remove from local keychain
2024 CKRecord* remove = items[0];
2025 NSString* removeAccount = [[self decryptRecord:remove] objectForKey:(__bridge id)kSecAttrAccount];
2026 XCTAssertNotNil(removeAccount, "received an account for the local delete object");
2028 NSURL* kcpath = (__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)@"keychain-2-debug.db");
2030 sqlite3_open([[kcpath path] UTF8String], &db);
2031 NSString* query = [NSString stringWithFormat:@"DELETE FROM genp WHERE uuid=\"%@\"", remove.recordID.recordName];
2032 char* sqlerror = NULL;
2033 XCTAssertEqual(SQLITE_OK, sqlite3_exec(db, [query UTF8String], NULL, NULL, &sqlerror), "SQL deletion shouldn't error");
2034 XCTAssertTrue(sqlerror == NULL, "No error string should have been returned: %s", sqlerror);
2036 sqlite3_free(sqlerror);
2041 // The second record is kept in-sync
2043 // Now, add an in-flight change (for record 3)
2044 [self holdCloudKitModifications];
2045 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2046 [self addGenericPassword:@"data" account:@"third"];
2047 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2049 // For the fourth, add a new record but prevent incoming queue processing
2050 self.keychainView.holdIncomingQueueOperation = [CKKSResultOperation named:@"hold-incoming" withBlock:^{}];
2052 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"fourth"];
2053 [self.keychainZone addToZone:ckr];
2054 [self.keychainView notifyZoneChange:nil];
2056 // Now, where are we....
2057 CKKSScanLocalItemsOperation* scanLocal = [self.keychainView scanLocalItems:@"test-scan"];
2058 [scanLocal waitUntilFinished];
2060 XCTAssertEqual(scanLocal.missingLocalItemsFound, 1u, "Should have found one missing item");
2062 // Allow everything to proceed
2063 [self releaseCloudKitModificationHold];
2064 [self.operationQueue addOperation:self.keychainView.holdIncomingQueueOperation];
2066 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2067 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2069 // And ensure that all four items are present again
2070 [self findGenericPassword: @"first" expecting: errSecSuccess];
2071 [self findGenericPassword: @"second" expecting: errSecSuccess];
2072 [self findGenericPassword: @"third" expecting: errSecSuccess];
2073 [self findGenericPassword: @"fourth" expecting: errSecSuccess];
2076 - (void)testResyncLocal {
2077 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2078 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2079 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2081 [self addGenericPassword: @"data" account: @"first"];
2082 [self addGenericPassword: @"data" account: @"second"];
2083 NSUInteger passwordCount = 2u;
2085 [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2086 [self startCKKSSubsystem];
2088 // Wait for uploads to happen
2089 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2090 [self waitForCKModifications];
2092 // Local resyncs shouldn't fetch clouds.
2093 self.silentFetchesAllowed = false;
2095 [self deleteGenericPassword:@"first"];
2096 [self deleteGenericPassword:@"second"];
2099 // And they're gone!
2100 [self findGenericPassword:@"first" expecting:errSecItemNotFound];
2101 [self findGenericPassword:@"second" expecting:errSecItemNotFound];
2103 CKKSLocalSynchronizeOperation* op = [self.keychainView resyncLocal];
2104 [op waitUntilFinished];
2105 XCTAssertNil(op.error, "Shouldn't be an error resyncing locally");
2107 // And they're back!
2108 [self checkGenericPassword: @"data" account: @"first"];
2109 [self checkGenericPassword: @"data" account: @"second"];
2112 - (void)testPlistRestoreResyncsLocal {
2113 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2114 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2115 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2117 [self addGenericPassword: @"data" account: @"first"];
2118 [self addGenericPassword: @"data" account: @"second"];
2119 NSUInteger passwordCount = 2u;
2121 [self checkGenericPassword: @"data" account: @"first"];
2122 [self checkGenericPassword: @"data" account: @"second"];
2124 [self expectCKModifyItemRecords:passwordCount currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2125 [self startCKKSSubsystem];
2127 // Wait for uploads to happen
2128 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2129 [self waitForCKModifications];
2132 // This 'restores' a plist keychain backup
2133 // That will kick off a local resync in CKKS, so hold that until we're ready...
2134 self.keychainView.holdLocalSynchronizeOperation = [CKKSResultOperation named:@"hold-local-synchronize" withBlock:^{}];
2136 // Local resyncs shouldn't fetch clouds.
2137 self.silentFetchesAllowed = false;
2139 CFErrorRef cferror = NULL;
2140 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
2141 CFErrorRef cfcferror = NULL;
2143 bool ret = SecServerImportKeychainInPlist(dbt, SecSecurityClientGet(), KEYBAG_NONE, KEYBAG_NONE,
2144 (__bridge CFDictionaryRef)@{}, kSecBackupableItemFilter, false, &cfcferror);
2146 XCTAssertNil(CFBridgingRelease(cfcferror), "Shouldn't error importing a 'backup'");
2147 XCTAssert(ret, "Importing a 'backup' should have succeeded");
2150 XCTAssertNil(CFBridgingRelease(cferror), "Shouldn't error mucking about in the db");
2152 // Restore is additive so original items stick around
2153 [self findGenericPassword:@"first" expecting:errSecSuccess];
2154 [self findGenericPassword:@"second" expecting:errSecSuccess];
2156 // Allow the local resync to continue...
2157 [self.operationQueue addOperation:self.keychainView.holdLocalSynchronizeOperation];
2158 [self.keychainView waitForOperationsOfClass:[CKKSLocalSynchronizeOperation class]];
2160 // Items are still here!
2161 [self checkGenericPassword: @"data" account: @"first"];
2162 [self checkGenericPassword: @"data" account: @"second"];
2165 - (void)testMultipleZoneAdd {
2166 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2167 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2169 // Bring up a new zone: we expect a key hierarchy upload.
2170 [self.injectedManager findOrCreateView:(id)kSecAttrViewHintAppleTV];
2171 CKRecordZoneID* appleTVZoneID = [[CKRecordZoneID alloc] initWithZoneName:(__bridge NSString*) kSecAttrViewHintAppleTV ownerName:CKCurrentUserDefaultName];
2172 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:appleTVZoneID];
2174 // We also expect the view manager's notifyNewTLKsInKeychain call to fire once (after some delay)
2175 OCMExpect([self.mockCKKSViewManager notifyNewTLKsInKeychain]);
2177 // Let the horses loose
2178 [self startCKKSSubsystem];
2180 // We expect a single record to be uploaded to the 'keychain' view
2181 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2182 [self addGenericPassword: @"data" account: @"account-delete-me"];
2183 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2185 // We expect a single record to be uploaded to the 'atv' view
2186 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:appleTVZoneID];
2187 [self addGenericPassword: @"atv"
2188 account: @"tvaccount"
2189 viewHint:(__bridge NSString*) kSecAttrViewHintAppleTV
2190 access:(id)kSecAttrAccessibleAfterFirstUnlock
2191 expecting:errSecSuccess message:@"AppleTV view-hinted object"];
2193 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2195 OCMVerifyAllWithDelay(self.mockCKKSViewManager, 10);
2198 - (void)testMultipleZoneDelete {
2199 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2200 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2202 [self startCKKSSubsystem];
2204 // We expect a single record to be uploaded.
2205 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2206 [self addGenericPassword: @"data" account: @"account-delete-me"];
2207 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2209 // Bring up a new zone: we expect a key hierarchy and an item.
2210 [self.injectedManager findOrCreateView:(id)kSecAttrViewHintAppleTV];
2211 CKRecordZoneID* appleTVZoneID = [[CKRecordZoneID alloc] initWithZoneName:(__bridge NSString*) kSecAttrViewHintAppleTV ownerName:CKCurrentUserDefaultName];
2212 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:appleTVZoneID];
2213 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:appleTVZoneID];
2215 [self addGenericPassword: @"atv"
2216 account: @"tvaccount"
2217 viewHint:(__bridge NSString*) kSecAttrViewHintAppleTV
2218 access:(id)kSecAttrAccessibleAfterFirstUnlock
2219 expecting:errSecSuccess
2220 message:@"AppleTV view-hinted object"];
2221 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2223 // We expect a single record to be deleted from the ATV zone
2224 [self expectCKDeleteItemRecords: 1 zoneID:appleTVZoneID];
2225 [self deleteGenericPassword:@"tvaccount"];
2226 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2228 // Now we expect a single record to be deleted from the test zone
2229 [self expectCKDeleteItemRecords: 1 zoneID:self.keychainZoneID];
2230 [self deleteGenericPassword:@"account-delete-me"];
2231 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2234 - (void)testRestartWithoutRefetch {
2235 // Restarting the CKKS operation should check that it's been 15 minutes since the last fetch before it fetches again. Simulate this.
2237 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2238 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2239 [self startCKKSSubsystem];
2241 [self.keychainView waitForKeyHierarchyReadiness];
2242 [self waitForCKModifications];
2243 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2245 // Tear down the CKKS object and disallow fetches
2246 [self.keychainView halt];
2247 self.silentFetchesAllowed = false;
2249 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2250 [self.keychainView waitForKeyHierarchyReadiness];
2251 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2253 // Okay, cool, rad, now let's set the date to be very long ago and check that there's positively a fetch
2254 [self.keychainView halt];
2255 self.silentFetchesAllowed = false;
2257 [self.keychainView dispatchSync: ^bool {
2258 NSError* error = nil;
2259 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
2261 XCTAssertNil(error, "no error pulling ckse from database");
2262 XCTAssertNotNil(ckse, "received a ckse");
2264 ckse.lastFetchTime = [NSDate distantPast];
2265 [ckse saveToDatabase: &error];
2266 XCTAssertNil(error, "no error saving to database");
2270 [self expectCKFetch];
2271 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2272 [self.keychainView waitForKeyHierarchyReadiness];
2273 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2276 - (void)testRecoverFromZoneCreationFailure {
2277 // Fail the zone creation.
2278 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
2279 [self failNextZoneCreation:self.keychainZoneID];
2281 // Spin up CKKS subsystem.
2282 [self startCKKSSubsystem];
2284 // The CKKS subsystem should figure out the issue, and fix it.
2285 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2287 [self.keychainView waitForKeyHierarchyReadiness];
2288 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2290 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2291 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2292 [self addGenericPassword: @"data" account: @"account-delete-me"];
2293 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2295 XCTAssertNil(self.zones[self.keychainZoneID].creationError, "Creation error was unset (and so CKKS probably dealt with the error");
2298 - (void)testRecoverFromZoneSubscriptionFailure {
2299 // Fail the zone subscription.
2300 [self failNextZoneSubscription:self.keychainZoneID];
2302 // Spin up CKKS subsystem.
2303 [self startCKKSSubsystem];
2305 // The CKKS subsystem should figure out the issue, and fix it.
2306 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2308 [self.keychainView waitForKeyHierarchyReadiness];
2309 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2311 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2312 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2313 [self addGenericPassword: @"data" account: @"account-delete-me"];
2314 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2316 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
2319 - (void)testRecoverFromZoneSubscriptionFailureDueToZoneNotExisting {
2320 // This is different from testRecoverFromZoneSubscriptionFailure, since the zone is gone. CKKS must attempt to re-create the zone.
2322 // Silently fail the zone creation
2323 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
2324 [self failNextZoneCreationSilently:self.keychainZoneID];
2326 // Spin up CKKS subsystem.
2327 [self startCKKSSubsystem];
2329 // The CKKS subsystem should figure out the issue, and fix it.
2330 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2332 [self.keychainView waitForKeyHierarchyReadiness];
2333 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2335 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2336 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2337 [self addGenericPassword: @"data" account: @"account-delete-me"];
2338 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2340 XCTAssertFalse(self.zones[self.keychainZoneID].flag, "Zone flag was reset");
2341 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
2344 - (void)testRecoverFromDeletedTLKWithStashedTLK {
2345 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
2347 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2348 NSError* error = nil;
2351 [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
2352 XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
2354 // And delete the non-stashed version
2355 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
2356 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
2358 // Spin up CKKS subsystem.
2359 [self startCKKSSubsystem];
2361 [self.keychainView waitForKeyHierarchyReadiness];
2362 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2364 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2365 [self addGenericPassword: @"data" account: @"account-delete-me"];
2366 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2368 // CKKS should recreate the syncable TLK.
2369 [self checkNSyncableTLKsInKeychain: 1];
2372 - (void)testRecoverFromDeletedTLKWithStashedTLKUponRestart {
2373 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
2375 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2376 // Spin up CKKS subsystem.
2377 [self startCKKSSubsystem];
2378 [self.keychainView waitForKeyHierarchyReadiness];
2380 // Tear down the CKKS object
2381 [self.keychainView halt];
2383 NSError* error = nil;
2386 [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
2387 XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
2389 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
2390 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
2392 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2393 [self.keychainView waitForKeyHierarchyReadiness];
2394 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2396 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2397 [self addGenericPassword: @"data" account: @"account-delete-me"];
2398 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2400 // CKKS should recreate the syncable TLK.
2401 [self checkNSyncableTLKsInKeychain: 1];
2404 - (void)testRecoverFromTLKWriteFailure {
2405 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
2406 // Test starts with nothing in CloudKit, and will fail the first TLK write.
2407 NSError* noNetwork = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}];
2408 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:noNetwork];
2410 // Spin up CKKS subsystem.
2411 [self startCKKSSubsystem];
2413 // The CKKS subsystem should figure out the issue, and fix it.
2414 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2416 [self.keychainView waitForKeyHierarchyReadiness];
2417 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2419 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2420 [self addGenericPassword: @"data" account: @"account-delete-me"];
2421 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2423 // A network failure creating new TLKs shouldn't delete the 'failed' syncable one.
2424 [self checkNSyncableTLKsInKeychain: 2];
2427 - (void)testRecoverFromTLKRace {
2428 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
2429 // Test starts with nothing in CloudKit, and will fail the first TLK write.
2430 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject: ^{
2431 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2434 // Spin up CKKS subsystem.
2435 [self startCKKSSubsystem];
2437 // The first TLK write should fail, and then our fake TLKs should be there in CloudKit.
2438 // It shouldn't write anything back up to CloudKit.
2439 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2441 // Now the TLKs arrive from the other device...
2442 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2443 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2444 [self.keychainView waitForKeyHierarchyReadiness];
2446 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2447 [self addGenericPassword: @"data" account: @"account-delete-me"];
2448 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2450 // A race failure creating new TLKs should delete the old syncable one.
2451 [self checkNSyncableTLKsInKeychain: 1];
2454 - (void)testRecoverFromNullCurrentKeyPointers {
2455 // The current key pointers in cloudkit shouldn't ever not exist if keys do. But, if they don't, CKKS must recover.
2457 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
2458 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2459 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2461 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
2462 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
2463 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2464 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2465 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2467 // Spin up CKKS subsystem.
2468 [self startCKKSSubsystem];
2470 // The CKKS subsystem should figure out the issue, and fix it.
2471 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
2473 [self.keychainView waitForKeyHierarchyReadiness];
2475 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2478 - (void)testRecoverFromNoCurrentKeyPointers {
2479 // The current key pointers in cloudkit shouldn't ever point to nil. But, if they do, CKKS must recover.
2481 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
2482 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2483 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2485 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
2486 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentTLKPointer.storedCKRecord.recordID], "Deleted TLK pointer from zone");
2487 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassAPointer.storedCKRecord.recordID], "Deleted class a pointer from zone");
2488 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassCPointer.storedCKRecord.recordID], "Deleted class c pointer from zone");
2490 // Spin up CKKS subsystem.
2491 [self startCKKSSubsystem];
2493 // The CKKS subsystem should figure out the issue, and fix it.
2494 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
2496 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
2498 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2501 - (void)testRecoverFromBadChangeTag {
2502 // 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.
2504 // Test starts with a broken key hierarchy in our fake CloudKit, but a (incorrectly) up-to-date change tag stored locally.
2505 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2506 SecCKKSTestSetDisableKeyNotifications(true); // Don't tell CKKS about this key material; we're pretending like this is a securityd restart
2507 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2508 SecCKKSTestSetDisableKeyNotifications(false);
2510 [self.keychainView dispatchSync: ^bool {
2511 NSError* error = nil;
2512 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainZoneID.zoneName];
2513 XCTAssertNotNil(ckse, "should have received a ckse");
2515 ckse.ckzonecreated = true;
2516 ckse.ckzonesubscribed = true;
2517 ckse.changeToken = self.keychainZone.currentChangeToken;
2519 [ckse saveToDatabase: &error];
2520 XCTAssertNil(error, "shouldn't have gotten an error saving to database");
2524 // The CKKS subsystem should try to write TLKs, but fail. It'll then upload a TLK share for the keys already in CloudKit
2525 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
2526 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2528 // Spin up CKKS subsystem.
2529 [self startCKKSSubsystem];
2530 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2532 // CKKS should then happily use the keys in CloudKit
2533 [self createClassCItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me"];
2534 [self createClassAItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me-class-a"];
2537 - (void)testRecoverFromDeletedKeysNewItem {
2538 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2539 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2541 [self startCKKSSubsystem];
2542 [self.keychainView waitForKeyHierarchyReadiness];
2544 // We expect a single class C record to be uploaded.
2545 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2546 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2548 [self addGenericPassword: @"data" account: @"account-delete-me"];
2549 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2551 [self waitForCKModifications];
2552 [self.keychainView waitUntilAllOperationsAreFinished];
2554 // Now, delete the local keys from the keychain (but leave the synced TLK)
2555 SecCKKSTestSetDisableKeyNotifications(true);
2556 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2557 (id)kSecClass : (id)kSecClassInternetPassword,
2558 (id)kSecAttrNoLegacy : @YES,
2559 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2560 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
2561 }), @"Deleting local keys");
2562 SecCKKSTestSetDisableKeyNotifications(false);
2564 NSError* error = nil;
2565 [self.keychainZoneKeys.classC loadKeyMaterialFromKeychain:&error];
2566 XCTAssertNotNil(error, "Error loading class C key material from keychain");
2568 // We expect a single class C record to be uploaded.
2569 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2570 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2572 [self addGenericPassword: @"datadata" account: @"account-no-keys"];
2573 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2575 // We expect a single class A record to be uploaded.
2576 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2577 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2578 [self addGenericPassword:@"asdf"
2579 account:@"account-class-A"
2581 access:(id)kSecAttrAccessibleWhenUnlocked
2582 expecting:errSecSuccess
2583 message:@"Adding class A item"];
2584 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2587 - (void)testRecoverFromDeletedKeysReceive {
2588 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2589 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2591 [self startCKKSSubsystem];
2592 [self.keychainView waitForKeyHierarchyReadiness];
2594 [self waitForCKModifications];
2595 [self.keychainView waitUntilAllOperationsAreFinished];
2597 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2599 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
2601 // Now, delete the local keys from the keychain (but leave the synced TLK)
2602 SecCKKSTestSetDisableKeyNotifications(true);
2603 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2604 (id)kSecClass : (id)kSecClassInternetPassword,
2605 (id)kSecAttrNoLegacy : @YES,
2606 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2607 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
2608 }), @"Deleting local keys");
2609 SecCKKSTestSetDisableKeyNotifications(false);
2611 // Trigger a notification (with hilariously fake data)
2612 [self.keychainZone addToZone: ckr];
2613 [self.keychainView notifyZoneChange:nil];
2614 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2615 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2617 [self findGenericPassword: @"account0" expecting:errSecSuccess];
2620 - (void)testRecoverDeletedTLK {
2621 // If the TLK disappears halfway through, well, that's no good. But we should recover using TLK sharing
2623 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2624 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2626 [self startCKKSSubsystem];
2627 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
2629 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2630 [self waitForCKModifications];
2632 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
2633 [self.keychainView waitUntilAllOperationsAreFinished];
2635 // Now, delete the local keys from the keychain
2636 SecCKKSTestSetDisableKeyNotifications(true);
2637 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2638 (id)kSecClass : (id)kSecClassInternetPassword,
2639 (id)kSecAttrNoLegacy : @YES,
2640 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2641 (id)kSecAttrSynchronizable : (id)kSecAttrSynchronizableAny,
2642 }), @"Deleting CKKS keys");
2643 SecCKKSTestSetDisableKeyNotifications(false);
2645 // Trigger a notification (with hilariously fake data)
2646 [self.keychainZone addToZone: ckr];
2647 [self.keychainView notifyZoneChange:nil];
2649 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2651 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should return to 'ready'");
2653 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // Do this again, to allow for non-atomic key state machinery switching
2655 [self findGenericPassword: @"account0" expecting:errSecSuccess];
2658 - (void)testRecoverMissingRolledKey {
2659 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2661 NSString* accountShouldExist = @"under-rolled-key";
2662 NSString* accountWillExist = @"under-rolled-key-later";
2663 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountShouldExist];
2664 [self.keychainZone addToZone: ckr];
2666 CKRecord* ckrAddedLater = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountWillExist];
2667 CKKSKey* pastClassCKey = self.keychainZoneKeys.classC;
2669 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2670 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2672 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2674 [self startCKKSSubsystem];
2675 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
2677 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2678 [self waitForCKModifications];
2680 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2681 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2682 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
2684 // Now, find and delete the class C key that ckrAddedLater is under
2685 NSError* error = nil;
2686 XCTAssertTrue([pastClassCKey deleteKeyMaterialFromKeychain:&error], "Should be able to delete old key material from keychain");
2687 XCTAssertNil(error, "Should be no error deleting old key material from keychain");
2689 [self.keychainZone addToZone:ckrAddedLater];
2690 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2692 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2693 [self findGenericPassword:accountWillExist expecting:errSecSuccess];
2695 XCTAssertTrue([pastClassCKey loadKeyMaterialFromKeychain:&error], "Class C key should be back in the keychain");
2696 XCTAssertNil(error, "Should be no error loading key from keychain");
2699 - (void)testRecoverMissingRolledClassAKeyWhileLocked {
2700 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2702 NSString* accountShouldExist = @"under-rolled-key";
2703 NSString* accountWillExist = @"under-rolled-key-later";
2704 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountShouldExist key:self.keychainZoneKeys.classA];
2705 [self.keychainZone addToZone: ckr];
2707 CKRecord* ckrAddedLater = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountWillExist key:self.keychainZoneKeys.classA];
2708 CKKSKey* pastClassAKey = self.keychainZoneKeys.classA;
2710 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2711 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2713 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2715 [self startCKKSSubsystem];
2716 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
2718 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2719 [self waitForCKModifications];
2721 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2722 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2723 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
2725 // Now, find and delete the class C key that ckrAddedLater is under
2726 NSError* error = nil;
2727 XCTAssertTrue([pastClassAKey deleteKeyMaterialFromKeychain:&error], "Should be able to delete old key material from keychain");
2728 XCTAssertNil(error, "Should be no error deleting old key material from keychain");
2730 // now, lock the keychain
2731 self.aksLockState = true;
2732 [self.lockStateTracker recheck];
2734 [self.keychainZone addToZone:ckrAddedLater];
2735 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2737 // Item should still not exist due to the lock state....
2738 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2739 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
2741 self.aksLockState = false;
2742 [self.lockStateTracker recheck];
2745 [self.keychainView waitUntilAllOperationsAreFinished];
2746 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2747 [self findGenericPassword:accountWillExist expecting:errSecSuccess];
2749 XCTAssertTrue([pastClassAKey loadKeyMaterialFromKeychain:&error], "Class A key should be back in the keychain");
2750 XCTAssertNil(error, "Should be no error loading key from keychain");
2753 - (void)testRecoverFromBadCurrentKeyPointer {
2754 // The current key pointers in cloudkit shouldn't ever point to missing entries. But, if they do, CKKS must recover.
2756 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
2757 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2758 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2760 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
2761 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
2762 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real tlk" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
2763 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class a key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
2764 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class c key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
2766 // Spin up CKKS subsystem.
2767 [self startCKKSSubsystem];
2769 // The CKKS subsystem should figure out the issue, and fix it (while uploading itself a TLK Share)
2770 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2772 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
2774 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2777 - (void)testRecoverFromIncorrectCurrentTLKPointer {
2778 // The current key pointers in cloudkit shouldn't ever point to wrong entries. But, if they do, CKKS must recover.
2780 // Test starts with a rolled hierarchy, and CKPs pointing to the wrong items
2781 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2782 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2784 CKKSCurrentKeyPointer* oldTLKCKP = self.keychainZoneKeys.currentTLKPointer;
2785 CKRecord* oldTLKPointer = [self.keychainZone.currentDatabase[self.keychainZoneKeys.currentTLKPointer.storedCKRecord.recordID] copy];
2787 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2788 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2790 ZoneKeys* newZoneKeys = [self.keychainZoneKeys copy];
2792 // And put the oldTLKPointer back
2793 [self.zones[self.keychainZoneID] addToZone:oldTLKPointer];
2794 self.keychainZoneKeys.currentTLKPointer = oldTLKCKP;
2796 // Make sure it stuck:
2797 XCTAssertNotEqualObjects(self.keychainZoneKeys.currentTLKPointer,
2798 newZoneKeys.currentTLKPointer,
2799 "current TLK pointer should now not point to proper TLK");
2801 // Spin up CKKS subsystem.
2802 [self startCKKSSubsystem];
2804 // The CKKS subsystem should figure out the issue, and fix it (while uploading itself a TLK Share)
2805 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
2807 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
2809 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2810 [self waitForCKModifications];
2812 XCTAssertEqualObjects(self.keychainZoneKeys.currentTLKPointer,
2813 newZoneKeys.currentTLKPointer,
2814 "current TLK pointer should now point to proper TLK");
2815 XCTAssertEqualObjects(self.keychainZoneKeys.currentClassAPointer,
2816 newZoneKeys.currentClassAPointer,
2817 "current Class A pointer should now point to proper Class A key");
2818 XCTAssertEqualObjects(self.keychainZoneKeys.currentClassCPointer,
2819 newZoneKeys.currentClassCPointer,
2820 "current Class C pointer should now point to proper Class C key");
2823 - (void)testRecoverFromCloudKitFetchFail {
2824 // Test starts with nothing in database, but one in our fake CloudKit.
2825 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2826 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
2828 // The first two CKRecordZoneChanges should fail with a 'network unavailable' error.
2829 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
2830 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
2832 // Spin up CKKS subsystem.
2833 [self startCKKSSubsystem];
2835 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2836 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2837 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2839 // We expect a single record to be uploaded
2840 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2841 [self addGenericPassword: @"data" account: @"account-delete-me"];
2842 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2844 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2845 [self addGenericPassword:@"asdf"
2846 account:@"account-class-A"
2848 access:(id)kSecAttrAccessibleWhenUnlocked
2849 expecting:errSecSuccess
2850 message:@"Adding class A item"];
2851 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2854 - (void)testRecoverFromCloudKitFetchNetworkFailAfterReady {
2855 // Test starts with nothing in database, but one in our fake CloudKit.
2856 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2858 // Spin up CKKS subsystem.
2859 [self startCKKSSubsystem];
2861 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
2862 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateReady, "CKKS entered ready");
2864 // Network is unavailable
2865 self.reachabilityFlags = 0;
2866 [self.reachabilityTracker recheck];
2868 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
2869 [self.keychainZone addToZone:ckr];
2871 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
2873 // Say network is available
2874 self.reachabilityFlags = kSCNetworkReachabilityFlagsReachable;
2875 [self.reachabilityTracker recheck];
2877 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2879 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
2882 - (void)testRecoverFromCloudKitFetchNetworkFailBeforeReady {
2883 // Test starts with nothing in database, but one in our fake CloudKit.
2884 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2886 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
2887 [self.keychainZone addToZone:ckr];
2889 // Network is unavailable
2890 self.reachabilityFlags = 0;
2891 [self.reachabilityTracker recheck];
2893 // Spin up CKKS subsystem.
2894 [self startCKKSSubsystem];
2896 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateInitializing] wait:20*NSEC_PER_SEC], "CKKS entered initializing");
2897 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateInitializing, "CKKS entered initializing");
2899 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2900 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2901 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2903 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
2905 // Say network is available
2906 self.reachabilityFlags = kSCNetworkReachabilityFlagsReachable;
2907 [self.reachabilityTracker recheck];
2909 [self.keychainView waitUntilAllOperationsAreFinished];
2910 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2912 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
2915 - (void)testWaitAfterCloudKitNetworkFailDuringOutgoingQueueOperation {
2916 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2918 [self startCKKSSubsystem];
2920 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered ready");
2922 // Network is now unavailable
2923 self.reachabilityFlags = 0;
2924 [self.reachabilityTracker recheck];
2926 NSError* noNetwork = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}];
2927 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:noNetwork];
2928 [self addGenericPassword: @"data" account: @"account-delete-me"];
2930 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2933 // Once network is available again, the write should happen
2934 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
2935 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2937 self.reachabilityFlags = kSCNetworkReachabilityFlagsReachable;
2938 [self.reachabilityTracker recheck];
2940 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
2942 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2945 - (void)testRecoverFromCloudKitFetchFailWithDelay {
2946 // Test starts with nothing in database, but one in our fake CloudKit.
2947 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2948 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
2950 // The first CKRecordZoneChanges should fail with a 'delay' error.
2951 self.silentFetchesAllowed = false;
2952 [self.keychainZone failNextFetchWith:[[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorRequestRateLimited userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:4]}]];
2953 [self expectCKFetch];
2955 // Spin up CKKS subsystem.
2956 [self startCKKSSubsystem];
2958 // Ensure it doesn't fetch within these three seconds (if it does, an exception will throw).
2961 // Okay, you can fetch again.
2962 [self expectCKFetch];
2964 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2965 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2966 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2968 // We expect a single record to be uploaded
2969 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2970 [self addGenericPassword: @"data" account: @"account-delete-me"];
2971 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2974 - (void)testRecoverFromCloudKitOldChangeToken {
2975 // Test starts with nothing in database, but one in our fake CloudKit.
2976 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2978 // Spin up CKKS subsystem.
2979 [self startCKKSSubsystem];
2981 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2982 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2983 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2985 // We expect a single record to be uploaded
2986 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2987 [self addGenericPassword: @"data" account: @"account-delete-me"];
2988 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2990 // Delete all old database states, to destroy the change tag validity
2991 [self.keychainZone.pastDatabases removeAllObjects];
2993 // We expect a total local flush and refetch
2994 self.silentFetchesAllowed = false;
2995 [self expectCKFetch]; // one to fail with a CKErrorChangeTokenExpired error
2996 [self expectCKFetch]; // and one to succeed
2998 // Trigger a fake change notification
2999 [self.keychainView notifyZoneChange:nil];
3001 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3003 // And check that a new upload happens just fine.
3004 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3005 [self addGenericPassword:@"asdf"
3006 account:@"account-class-A"
3008 access:(id)kSecAttrAccessibleWhenUnlocked
3009 expecting:errSecSuccess
3010 message:@"Adding class A item"];
3011 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3014 - (void)testRecoverFromCloudKitUnknownDeviceStateRecord {
3015 // Test starts with nothing in database, but one in our fake CloudKit.
3016 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3017 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3018 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3020 // Save a new device state record with some fake etag
3021 [self.keychainView dispatchSync: ^bool {
3022 CKKSDeviceStateEntry* cdse = [[CKKSDeviceStateEntry alloc] initForDevice:self.ckDeviceID
3023 osVersion:@"fake-record"
3024 lastUnlockTime:[NSDate date]
3025 circlePeerID:self.circlePeerID
3026 circleStatus:kSOSCCInCircle
3027 keyState:SecCKKSZoneKeyStateWaitForTLK
3029 currentClassAUUID:nil
3030 currentClassCUUID:nil
3031 zoneID:self.keychainZoneID
3032 encodedCKRecord:nil];
3033 XCTAssertNotNil(cdse, "Should have created a fake CDSE");
3034 CKRecord* record = [cdse CKRecordWithZoneID:self.keychainZoneID];
3035 XCTAssertNotNil(record, "Should have created a fake CDSE CKRecord");
3036 record.etag = @"fake etag";
3037 cdse.storedCKRecord = record;
3039 NSError* error = nil;
3040 [cdse saveToDatabase:&error];
3041 XCTAssertNil(error, @"No error saving cdse to database");
3046 // Spin up CKKS subsystem.
3047 [self startCKKSSubsystem];
3049 // We expect a record failure, since the device state record is broke
3050 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
3052 // And then we expect a clean write
3053 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
3054 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3056 [self addGenericPassword: @"data" account: @"account-delete-me"];
3057 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3060 - (void)testRecoverFromCloudKitUnknownItemRecord {
3061 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3063 // Spin up CKKS subsystem.
3064 [self startCKKSSubsystem];
3066 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3068 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
3069 [self.keychainZone addToZone:ckr];
3071 [self.keychainView notifyZoneChange:nil];
3072 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3074 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3076 // Delete the record from CloudKit, but miss the notification
3077 XCTAssertNil([self.keychainZone deleteCKRecordIDFromZone: ckr.recordID], "Deleting the record from fake CloudKit should succeed");
3079 // Expect a failed upload when we modify the item
3080 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
3081 [self updateGenericPassword:@"never seen again" account:@"account-delete-me"];
3082 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3084 [self.keychainView waitUntilAllOperationsAreFinished];
3086 // And the item should be disappeared from the local keychain
3087 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3090 - (void)testRecoverFromCloudKitUserDeletedZone {
3091 // Test starts with nothing in database, but one in our fake CloudKit.
3092 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3094 // Spin up CKKS subsystem.
3095 [self startCKKSSubsystem];
3097 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3098 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3099 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3101 // We expect a single record to be uploaded
3102 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3103 [self addGenericPassword: @"data" account: @"account-delete-me"];
3104 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3106 // The first CKRecordZoneChanges should fail with a 'CKErrorUserDeletedZone' error. This will cause a local reset, ending up with zone re-creation.
3107 self.zones[self.keychainZoneID] = nil; // delete the zone
3108 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorUserDeletedZone userInfo:@{}]];
3110 // We expect CKKS to recreate the zone, then perform a key hierarchy upload, and then the class C item upload
3111 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3112 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3114 [self.keychainView notifyZoneChange:nil];
3116 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3118 // And check that a new upload occurs.
3119 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3121 [self addGenericPassword:@"asdf"
3122 account:@"account-class-A"
3124 access:(id)kSecAttrAccessibleWhenUnlocked
3125 expecting:errSecSuccess
3126 message:@"Adding class A item"];
3127 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3130 - (void)testRecoverFromCloudKitZoneNotFoundWithoutZoneDeletion {
3131 // Test starts with nothing in database, but one in our fake CloudKit.
3132 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3133 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
3135 // Spin up CKKS subsystem.
3136 [self startCKKSSubsystem];
3138 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3139 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3140 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3142 // We expect a single record to be uploaded
3143 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3144 [self addGenericPassword: @"data" account: @"account-delete-me"];
3145 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3147 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
3149 [self waitForCKModifications];
3150 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
3152 // The next CKRecordZoneChanges will fail with a 'zone not found' error.
3153 self.zones[self.keychainZoneID] = nil; // delete the zone
3155 // We expect CKKS to reset itself and recover, then a key hierarchy upload, and then the class C item upload
3156 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3157 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3159 [self.keychainView notifyZoneChange:nil];
3160 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3161 [self waitForCKModifications];
3163 // And check that a new upload occurs.
3164 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3166 [self addGenericPassword:@"asdf"
3167 account:@"account-class-A"
3169 access:(id)kSecAttrAccessibleWhenUnlocked
3170 expecting:errSecSuccess
3171 message:@"Adding class A item"];
3172 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3175 - (void)testRecoverFromCloudKitZoneNotFoundFetchBeforeSigninOccurs {
3176 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
3178 // Before CKKS sign-in, it receives a fetch rpc
3179 XCTestExpectation *fetchReturns = [self expectationWithDescription:@"fetch returned"];
3180 [self.injectedManager rpcFetchAndProcessChanges:nil reply:^(NSError *result) {
3181 XCTAssertNil(result, "Should be no error fetching and processing changes");
3182 [fetchReturns fulfill];
3185 // start 'login'. CKKS Should upload a key hierarchy
3186 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
3187 [self startCKKSSubsystem];
3189 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
3191 // We expect a single record to be uploaded
3192 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
3193 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3194 [self addGenericPassword: @"data" account: @"account-delete-me"];
3195 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3197 // The fetch should have come back by now
3198 [self waitForExpectations: @[fetchReturns] timeout:5];
3201 - (void)testNoCloudKitAccount {
3202 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
3203 self.accountStatus = CKAccountStatusNoAccount;
3204 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCNotInCircle error:nil];;
3205 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3207 self.silentFetchesAllowed = false;
3208 [self startCKKSSubsystem];
3210 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3212 [self addGenericPassword: @"data" account: @"account-delete-me"];
3213 [self.keychainView waitUntilAllOperationsAreFinished];
3215 // simulate a NSNotification callback (but still logged out)
3216 self.accountStatus = CKAccountStatusNoAccount;
3217 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3219 // There should be no further uploads, even when we save keychain items
3220 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3221 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3223 [self.keychainView waitUntilAllOperationsAreFinished];
3224 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3226 // Test that there are no items in the database (since we never logged in)
3227 [self checkNoCKKSData: self.keychainView];
3230 - (void)testSACloudKitAccount {
3231 // Test starts with nothing in database and the user logged into CloudKit and in circle, but the account is not HSA2.
3232 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];
3233 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3235 self.accountStatus = CKAccountStatusAvailable;
3236 self.supportsDeviceToDeviceEncryption = NO;
3238 self.silentFetchesAllowed = false;
3239 [self startCKKSSubsystem];
3241 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3242 XCTAssertNotNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's no account");
3243 XCTAssertEqualObjects(self.accountStateTracker.currentAccountError.domain, CKKSErrorDomain, "Account tracker error should be in CKKSErrorDomain");
3244 XCTAssertEqual(self.accountStateTracker.currentAccountError.code, CKKSNotHSA2, "Account tracker error should be upset about HSA2");
3246 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3248 // There should be no uploads, even when we save keychain items and enter/exit circle
3249 [self addGenericPassword: @"data" account: @"account-delete-me"];
3250 [self.keychainView waitUntilAllOperationsAreFinished];
3252 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCNotInCircle error:nil];
3253 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3254 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3256 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];
3257 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3258 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3260 [self.keychainView waitUntilAllOperationsAreFinished];
3261 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3263 // Test that there are no items in the database (since we never were in an HSA2 account)
3264 [self checkNoCKKSData: self.keychainView];
3267 - (void)testNoCircle {
3268 // Test starts with nothing in database and the user logged into CloudKit, but out of Circle.
3269 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCNotInCircle error:nil];
3270 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3272 self.accountStatus = CKAccountStatusAvailable;
3274 self.silentFetchesAllowed = false;
3275 [self startCKKSSubsystem];
3277 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3278 XCTAssertNotNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's no account");
3279 XCTAssertEqualObjects(self.accountStateTracker.currentAccountError.domain, (__bridge NSString*)kSOSErrorDomain, "Account tracker error should be in SOSErrorDomain");
3280 XCTAssertEqual(self.accountStateTracker.currentAccountError.code, kSOSErrorNotInCircle, "Account tracker error should be upset about out-of-circle");
3282 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3284 [self addGenericPassword: @"data" account: @"account-delete-me"];
3285 [self.keychainView waitUntilAllOperationsAreFinished];
3287 // simulate a NSNotification callback (but still logged out)
3288 self.accountStatus = CKAccountStatusNoAccount;
3289 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3291 // There should be no further uploads, even when we save keychain items
3292 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3293 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3295 [self.keychainView waitUntilAllOperationsAreFinished];
3296 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3298 // Test that there are no items in the database (since we never logged in)
3299 [self checkNoCKKSData: self.keychainView];
3302 - (void)testCloudKitLogin {
3303 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
3304 self.accountStatus = CKAccountStatusNoAccount;
3305 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCNotInCircle error:nil];
3306 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3308 // Before we inform CKKS of its account state....
3309 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
3311 [self startCKKSSubsystem];
3313 XCTAssertEqual(0, [self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], "Should have been told of a 'logout' event on startup");
3314 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event shouldn't have happened");
3315 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3317 [self.keychainView waitUntilAllOperationsAreFinished];
3318 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3320 XCTAssertNotNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's no account");
3321 XCTAssertEqualObjects(self.accountStateTracker.currentAccountError.domain, CKKSErrorDomain, "Account tracker error should be in CKKSErrorDomain");
3322 XCTAssertEqual(self.accountStateTracker.currentAccountError.code, CKKSNotLoggedIn, "Account tracker error should just be 'no account'");
3324 // simulate a cloudkit login and NSNotification callback
3325 self.accountStatus = CKAccountStatusAvailable;
3326 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3328 XCTAssertNotNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's no account");
3329 XCTAssertEqualObjects(self.accountStateTracker.currentAccountError.domain, (__bridge NSString*)kSOSErrorDomain, "Account tracker error should be in SOSErrorDomain");
3330 XCTAssertEqual(self.accountStateTracker.currentAccountError.code, kSOSErrorNotInCircle, "Account tracker error should be upset about out-of-circle");
3332 // No writes yet, since we're not in circle
3333 [self.keychainView waitUntilAllOperationsAreFinished];
3334 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3336 // We expect some sort of TLK/key hierarchy upload once we are notified of entering the circle.
3337 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3339 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];
3340 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3342 XCTAssertNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's an account");
3344 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3345 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3346 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3348 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3349 [self waitForCKModifications];
3351 // We expect a single class C record to be uploaded.
3352 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3353 [self addGenericPassword: @"data" account: @"account-delete-me"];
3355 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3356 [self waitForCKModifications];
3359 - (void)testCloudKitLogoutLogin {
3360 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
3361 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3363 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
3364 [self startCKKSSubsystem];
3365 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3366 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3367 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3369 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3370 [self waitForCKModifications];
3372 // We expect a single class C record to be uploaded.
3373 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3374 [self addGenericPassword: @"data" account: @"account-delete-me"];
3376 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3377 [self waitForCKModifications];
3379 // simulate a cloudkit logout and NSNotification callback
3380 self.accountStatus = CKAccountStatusNoAccount;
3381 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3382 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCNotInCircle error:nil];
3383 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3385 XCTAssertNotNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's no account");
3386 XCTAssertEqualObjects(self.accountStateTracker.currentAccountError.domain, CKKSErrorDomain, "Account tracker error should be in CKKSErrorDomain");
3387 XCTAssertEqual(self.accountStateTracker.currentAccountError.code, CKKSNotLoggedIn, "Account tracker error should just believe we're not logged in");
3389 // Test that there are no items in the database after logout
3390 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
3391 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
3392 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3393 [self checkNoCKKSData: self.keychainView];
3395 // There should be no further uploads, even when we save keychain items
3396 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3397 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3399 [self.keychainView waitUntilAllOperationsAreFinished];
3400 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3401 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
3403 // simulate a cloudkit login
3404 // 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
3405 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
3407 self.accountStatus = CKAccountStatusAvailable;
3408 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3409 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];
3410 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3411 XCTAssertNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's an account");
3413 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3414 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3415 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3417 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3419 // Let everything settle...
3420 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3421 [self waitForCKModifications];
3424 self.accountStatus = CKAccountStatusNoAccount;
3425 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3426 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCNotInCircle error:nil];
3427 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3429 // Test that there are no items in the database after logout
3430 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
3431 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
3432 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3433 [self checkNoCKKSData: self.keychainView];
3435 // There should be no further uploads, even when we save keychain items
3436 [self addGenericPassword: @"data" account: @"account-delete-me-5"];
3437 [self addGenericPassword: @"data" account: @"account-delete-me-6"];
3439 [self.keychainView waitUntilAllOperationsAreFinished];
3440 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3442 // simulate a cloudkit login
3443 // 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
3444 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
3446 self.accountStatus = CKAccountStatusAvailable;
3447 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3448 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];
3449 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3451 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3452 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3453 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3455 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3457 // Let everything settle...
3458 [self.keychainView waitUntilAllOperationsAreFinished];
3459 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3462 self.accountStatus = CKAccountStatusNoAccount;
3463 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3464 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCNotInCircle error:nil];
3465 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3467 // Test that there are no items in the database after logout
3468 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
3469 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
3470 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3471 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
3472 [self checkNoCKKSData: self.keychainView];
3474 // Force zone into error state
3475 self.keychainView.keyHierarchyState = SecCKKSZoneKeyStateError;
3477 self.accountStatus = CKAccountStatusAvailable;
3478 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3479 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];
3480 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3482 XCTestExpectation *operationRun = [self expectationWithDescription:@"operation run"];
3483 NSOperation* op = [NSBlockOperation named:@"test" withBlock:^{
3484 [operationRun fulfill];
3487 [op addDependency:self.keychainView.keyStateReadyDependency];
3488 [self.operationQueue addOperation:op];
3490 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3491 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3492 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3494 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3495 [self waitForExpectations: @[operationRun] timeout:5];
3496 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3499 - (void)testCloudKitLogoutDueToGreyMode {
3500 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
3501 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
3503 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
3504 [self startCKKSSubsystem];
3505 XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "Should have been told of a 'login'");
3506 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:50*NSEC_PER_MSEC], "'logout' event should be reset");
3507 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3509 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
3511 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3512 [self waitForCKModifications];
3514 // simulate a cloudkit grey mode switch and NSNotification callback. CKKS should treat this as a logout
3515 self.iCloudHasValidCredentials = false;
3516 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3518 XCTAssertNotNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's no account");
3519 XCTAssertEqualObjects(self.accountStateTracker.currentAccountError.domain, CKKSErrorDomain, "Account tracker error should be in CKKSErrorDomain");
3520 XCTAssertEqual(self.accountStateTracker.currentAccountError.code, CKKSiCloudGreyMode, "Account tracker error should be upset about grey mode");
3522 // Test that there are no items in the database after logout
3523 XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "Should have been told of a 'logout'");
3524 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:50*NSEC_PER_MSEC], "'login' event should be reset");
3525 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3526 [self checkNoCKKSData: self.keychainView];
3527 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
3529 // There should be no further uploads, even when we save keychain items
3530 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3531 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3533 [self.keychainView waitUntilAllOperationsAreFinished];
3534 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3536 // Also, fetches shouldn't occur
3537 self.silentFetchesAllowed = false;
3538 NSOperation* op = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
3539 CKKSResultOperation* timeoutOp = [CKKSResultOperation named:@"timeout" withBlock:^{}];
3540 [timeoutOp addDependency:op];
3541 [timeoutOp timeout:4*NSEC_PER_SEC];
3542 [self.operationQueue addOperation:timeoutOp];
3543 [timeoutOp waitUntilFinished];
3545 // CloudKit figures its life out. We expect the two passwords from before to be uploaded
3546 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
3547 self.silentFetchesAllowed = true;
3548 self.iCloudHasValidCredentials = true;
3549 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3551 XCTAssertNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's an account");
3553 XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "Should have been told of a 'login'");
3554 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:50*NSEC_PER_MSEC], "'logout' event should be reset");
3555 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3556 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3558 // And fetching still works!
3559 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D00" withAccount:@"account0"]];
3560 [self.keychainView notifyZoneChange:nil];
3561 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3562 [self findGenericPassword: @"account0" expecting:errSecSuccess];
3563 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3566 - (void)testCloudKitLoginRace {
3567 // Test starts with nothing in database, and 'in circle', but securityd hasn't received notification if we're logged into CloudKit.
3568 // CKKS should not call handleLogout.
3570 id partialKVMock = OCMPartialMock(self.keychainView);
3571 OCMReject([partialKVMock handleCKLogout]);
3572 // note: don't unblock the ck account state object yet...
3574 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];
3575 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3577 // Add a keychain item, but make sure it doesn't upload yet.
3578 [self addGenericPassword: @"data" account: @"account-delete-me"];
3580 [self.keychainView waitUntilAllOperationsAreFinished];
3581 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3583 // Now that we're here (and handleCKLogout hasn't been called), bring the account up
3585 // We expect some sort of TLK/key hierarchy upload once we are notified of entering the circle.
3586 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3588 // We expect a single class C record to be uploaded.
3589 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3591 self.accountStatus = CKAccountStatusAvailable;
3592 [self startCKAccountStatusMock];
3594 // simulate another NSNotification callback
3595 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3597 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3598 [self waitForCKModifications];
3600 // Make sure new items upload too
3601 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3602 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3603 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3605 [self.keychainView waitUntilAllOperationsAreFinished];
3606 [self waitForCKModifications];
3607 [self.keychainView halt];
3609 [partialKVMock stopMocking];
3612 - (void)testDontLogOutIfBeforeFirstUnlock {
3613 // test starts as if a previously logged-in device has just rebooted
3614 self.aksLockState = true;
3615 self.accountStatus = CKAccountStatusAvailable;
3617 // This is the original state of the account tracker
3618 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCError error:nil];
3619 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3621 // And this is what the first circle status fetch will actually return
3622 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCError error:[NSError errorWithDomain:(__bridge id)kSOSErrorDomain code:kSOSErrorNotReady description:@"fake error: device is locked, so SOS doesn't know if it's in-circle"]];
3623 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3625 XCTAssertNil(self.accountStateTracker.currentAccountError, "Account tracker error should not yet exist");
3626 XCTAssertEqual(self.accountStateTracker.currentComputedAccountStatus, CKKSAccountStatusUnknown, "Account tracker status should just be 'no account'");
3627 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKKS shouldn't know the account state yet");
3629 [self startCKKSSubsystem];
3631 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "Shouldn't have been told of a 'logout' event on startup");
3632 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event shouldn't have happened");
3633 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKKS shouldn't know the account state yet");
3635 // And assume another CK status change
3636 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3637 XCTAssertEqual(self.accountStateTracker.currentComputedAccountStatus, CKKSAccountStatusUnknown, "Account tracker status should just be 'no account'");
3638 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKKS shouldn't know the account state yet");
3640 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3642 self.aksLockState = false;
3643 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];
3644 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3646 XCTAssertNil(self.accountStateTracker.currentAccountError, "Account state tracker should believe there's an account");
3648 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3649 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3650 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3652 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3653 [self waitForCKModifications];
3655 // We expect a single class C record to be uploaded.
3656 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3657 [self addGenericPassword: @"data" account: @"account-delete-me"];
3659 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3660 [self waitForCKModifications];
3663 - (void)testSyncableItemsAddedWhileLoggedOut {
3664 // Test that once CKKS is up and 'logged out', nothing happens when syncable items are added
3665 self.accountStatus = CKAccountStatusNoAccount;
3666 [self startCKAccountStatusMock];
3668 XCTAssertEqual([self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], 0, "CKKS should be told that it's logged out");
3670 // CKKS shouldn't decide to poke its state machine, but it should still send the notification
3671 XCTestExpectation* viewChangeNotification = [self expectChangeForView:self.keychainZoneID.zoneName];
3673 // Reject all attempts to trigger a state machine update
3674 id pokeKeyStateMachineScheduler = OCMClassMock([CKKSNearFutureScheduler class]);
3675 OCMReject([pokeKeyStateMachineScheduler trigger]);
3676 self.keychainView.pokeKeyStateMachineScheduler = pokeKeyStateMachineScheduler;
3678 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3680 [self waitForExpectations:@[viewChangeNotification] timeout:8];
3681 [pokeKeyStateMachineScheduler stopMocking];
3685 - (void)testNotStuckAfterReset {
3686 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3688 XCTestExpectation *operationRun = [self expectationWithDescription:@"operation run"];
3689 NSOperation* op = [NSBlockOperation named:@"test" withBlock:^{
3690 [operationRun fulfill];
3693 [op addDependency:self.keychainView.keyStateReadyDependency];
3694 [self.operationQueue addOperation:op];
3696 // And handle a spurious logout
3697 [self.keychainView handleCKLogout];
3699 [self startCKKSSubsystem];
3701 [self waitForExpectations: @[operationRun] timeout:20];
3704 - (void)testCKKSControlBringup {
3705 NSXPCInterface *interface = CKKSSetupControlProtocol([NSXPCInterface interfaceWithProtocol:@protocol(CKKSControlProtocol)]);
3706 XCTAssertNotNil(interface, "Received a configured CKKS interface");