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 "keychain/securityd/SecItemDb.h"
34 #include "keychain/securityd/SecItemServer.h"
35 #include <utilities/SecFileLocations.h>
36 #include "keychain/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"
55 #import "keychain/ckks/CKKSPeer.h"
57 #import "keychain/ckks/tests/MockCloudKit.h"
59 #import "keychain/ckks/tests/CKKSTests.h"
60 #import <utilities/SecCoreAnalytics.h>
63 @interface CKKSLockStateTracker ()
64 @property (nullable) NSDate* lastUnlockedTime;
67 @implementation CloudKitKeychainSyncingTests
71 - (void)testBringupToKeyStateReady {
72 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
73 [self startCKKSSubsystem];
75 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
79 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
81 // We expect a single record to be uploaded.
82 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
84 [self startCKKSSubsystem];
85 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
87 [self addGenericPassword: @"data" account: @"account-delete-me"];
89 OCMVerifyAllWithDelay(self.mockDatabase, 20);
92 - (void)testActiveTLKS {
93 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
95 // We expect a single record to be uploaded.
96 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
98 [self startCKKSSubsystem];
100 [self addGenericPassword: @"data" account: @"account-delete-me"];
102 OCMVerifyAllWithDelay(self.mockDatabase, 20);
104 NSDictionary<NSString *,NSString *>* tlks = [[CKKSViewManager manager] activeTLKs];
106 XCTAssertEqual([tlks count], (NSUInteger)1, "One TLK");
107 XCTAssertNotNil(tlks[@"keychain"], "keychain have a UUID");
111 - (void)testAddMultipleItems {
112 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
113 [self startCKKSSubsystem];
115 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
116 [self addGenericPassword: @"data" account: @"account-delete-me"];
117 OCMVerifyAllWithDelay(self.mockDatabase, 20);
119 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
120 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
121 OCMVerifyAllWithDelay(self.mockDatabase, 20);
123 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
124 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
125 OCMVerifyAllWithDelay(self.mockDatabase, 20);
128 - (void)testAddItemWithoutUUID {
129 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
130 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
131 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
132 [self saveTLKMaterialToKeychain:self.keychainZoneID];
134 [self startCKKSSubsystem];
136 [self.keychainView waitUntilAllOperationsAreFinished];
138 SecCKKSTestSetDisableAutomaticUUID(true);
139 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
141 SecCKKSTestSetDisableAutomaticUUID(false);
143 // We then expect an upload of the added item
144 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
146 OCMVerifyAllWithDelay(self.mockDatabase, 20);
149 - (void)testModifyItem {
150 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
152 NSString* account = @"account-delete-me";
154 [self startCKKSSubsystem];
156 // We expect a single record to be uploaded.
157 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
158 [self addGenericPassword: @"data" account: account];
159 OCMVerifyAllWithDelay(self.mockDatabase, 20);
161 // And then modified.
162 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
163 [self updateGenericPassword: @"otherdata" account:account];
164 OCMVerifyAllWithDelay(self.mockDatabase, 20);
167 - (void)testModifyItemImmediately {
168 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
169 NSString* account = @"account-delete-me";
171 [self startCKKSSubsystem];
172 [self holdCloudKitModifications];
174 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
175 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
176 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
177 [self addGenericPassword: @"data" account: account];
178 OCMVerifyAllWithDelay(self.mockDatabase, 20);
180 // Right now, the write in CloudKit is pending. Make the local modification...
181 [self updateGenericPassword: @"otherdata" account:account];
183 // And then schedule the update
184 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
185 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
186 [self releaseCloudKitModificationHold];
188 OCMVerifyAllWithDelay(self.mockDatabase, 20);
191 - (void)testModifyItemPrimaryKey {
192 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
194 NSString* account = @"account-delete-me";
196 [self startCKKSSubsystem];
198 // We expect a single record to be uploaded.
199 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
200 [self addGenericPassword: @"data" account: account];
201 OCMVerifyAllWithDelay(self.mockDatabase, 20);
203 // And then modified. Since we're changing the "primary key", we expect to delete the old record and upload a new one.
204 [self expectCKModifyItemRecords:1 deletedRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:nil];
205 [self updateAccountOfGenericPassword: @"new-account-delete-me" account:account];
206 OCMVerifyAllWithDelay(self.mockDatabase, 20);
209 - (void)testModifyItemDuringReencrypt {
210 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
211 NSString* account = @"account-delete-me";
213 [self startCKKSSubsystem];
214 [self holdCloudKitModifications];
216 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
217 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
218 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
219 [self addGenericPassword: @"data" account: account];
220 OCMVerifyAllWithDelay(self.mockDatabase, 20);
222 // Right now, the write in CloudKit is pending. Make the local modification...
223 [self updateGenericPassword: @"otherdata" account:account];
225 // And then schedule the update
226 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
227 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
229 // Stop the reencrypt operation from happening
230 self.keychainView.holdReencryptOutgoingItemsOperation = [CKKSGroupOperation named:@"reencrypt-hold" withBlock: ^{
231 secnotice("ckks", "releasing reencryption hold");
234 // The cloudkit operation finishes, letting the next OQO proceed (and set up the reencryption operation)
235 [self releaseCloudKitModificationHold];
237 // And wait for this to finish...
238 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
239 // And once more to quiesce.
240 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
242 // Pause outgoing queue operations to ensure the reencryption operation runs first
243 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
244 secnotice("ckks", "releasing outgoing-queue hold");
247 // Run the reencrypt items operation to completion.
248 [self.operationQueue addOperation: self.keychainView.holdReencryptOutgoingItemsOperation];
249 [self.keychainView waitForOperationsOfClass:[CKKSReencryptOutgoingItemsOperation class]];
251 [self.operationQueue addOperation: self.keychainView.holdOutgoingQueueOperation];
253 OCMVerifyAllWithDelay(self.mockDatabase, 20);
254 [self.keychainView waitUntilAllOperationsAreFinished];
255 [self waitForCKModifications];
258 - (void)testModifyItemBeforeReencrypt {
259 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
260 NSString* account = @"account-delete-me";
262 [self startCKKSSubsystem];
263 [self holdCloudKitModifications];
265 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
266 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
267 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
268 [self addGenericPassword: @"data" account: account];
269 OCMVerifyAllWithDelay(self.mockDatabase, 20);
271 // Right now, the write in CloudKit is pending. Make the local modification...
272 [self updateGenericPassword: @"otherdata" account:account];
274 // And then schedule the update, but for the final version of the password
275 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
276 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"third"]];
278 // Stop the reencrypt operation from happening
279 self.keychainView.holdReencryptOutgoingItemsOperation = [CKKSGroupOperation named:@"reencrypt-hold" withBlock: ^{
280 secnotice("ckks", "releasing reencryption hold");
283 // The cloudkit operation finishes, letting the next OQO proceed (and set up the reencryption operation)
284 [self releaseCloudKitModificationHold];
286 // And wait for this to finish...
287 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
288 // And once more to quiesce.
289 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
291 [self updateGenericPassword: @"third" account:account];
293 // Item should upload.
294 OCMVerifyAllWithDelay(self.mockDatabase, 20);
296 // Run the reencrypt items operation to completion.
297 [self.operationQueue addOperation: self.keychainView.holdReencryptOutgoingItemsOperation];
298 [self.keychainView waitForOperationsOfClass:[CKKSReencryptOutgoingItemsOperation class]];
300 [self.keychainView waitUntilAllOperationsAreFinished];
301 [self waitForCKModifications];
304 - (void)testModifyItemDuringNetworkFailure {
305 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
306 NSString* account = @"account-delete-me";
308 [self startCKKSSubsystem];
309 [self holdCloudKitModifications];
311 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
312 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
314 [self addGenericPassword: @"data" account: account];
315 OCMVerifyAllWithDelay(self.mockDatabase, 20);
317 // Right now, the write in CloudKit is pending. Make the local modification...
318 [self updateGenericPassword: @"otherdata" account:account];
320 // And then schedule the update, but for the final version of the password
321 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
322 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
324 // The cloudkit operation finishes, letting the next OQO proceed (and set up uploading the new item)
325 [self releaseCloudKitModificationHold];
327 // Item should upload.
328 OCMVerifyAllWithDelay(self.mockDatabase, 20);
330 [self.keychainView waitUntilAllOperationsAreFinished];
331 [self waitForCKModifications];
334 - (void)testOutgoingQueueRecoverFromStaleInflightEntry {
335 // CKKS is restarting with an existing in-flight OQE
336 // Note that this test is incomplete, and doesn't re-add the item to the local keychain
337 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
338 NSString* account = @"fake-account";
340 [self.keychainView dispatchSync:^bool {
341 NSError* error = nil;
343 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
345 CKKSItem* item = [self newItem:ckrid withNewItemData:[self fakeRecordDictionary:account zoneID:self.keychainZoneID] key:self.keychainZoneKeys.classC];
346 XCTAssertNotNil(item, "Should be able to create a new fake item");
348 CKKSOutgoingQueueEntry* oqe = [[CKKSOutgoingQueueEntry alloc] initWithCKKSItem:item action:SecCKKSActionAdd state:SecCKKSStateInFlight waitUntil:nil accessGroup:@"ckks"];
349 XCTAssertNotNil(oqe, "Should be able to create a new fake OQE");
350 [oqe saveToDatabase:&error];
352 XCTAssertNil(error, "Shouldn't error saving new OQE to database");
356 NSError *error = NULL;
357 XCTAssertEqual([CKKSOutgoingQueueEntry countByState:SecCKKSStateInFlight zone:self.keychainZoneID error:&error], 1,
358 "Expected on inflight entry in outgoing queue: %@", error);
360 // When CKKS restarts, it should find and re-upload this item
361 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
362 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
364 [self startCKKSSubsystem];
365 [self.keychainView waitForFetchAndIncomingQueueProcessing];
367 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
368 [self beginSOSTrustedViewOperation:self.keychainView];
369 [self.keychainView waitForKeyHierarchyReadiness];
370 OCMVerifyAllWithDelay(self.mockDatabase, 20);
373 - (void)testOutgoingQueueRecoverFromNetworkFailure {
374 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
375 NSString* account = @"account-delete-me";
377 [self startCKKSSubsystem];
378 [self holdCloudKitModifications];
380 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
382 NSError* greyMode = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNotAuthenticated userInfo:@{
383 CKErrorRetryAfterKey: @(0.2),
385 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:greyMode];
387 [self addGenericPassword: @"data" account: account];
388 OCMVerifyAllWithDelay(self.mockDatabase, 20);
390 // And then schedule the retried update
391 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
392 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
394 // The cloudkit operation finishes, letting the next OQO proceed (and set up uploading the new item)
395 [self releaseCloudKitModificationHold];
397 OCMVerifyAllWithDelay(self.mockDatabase, 20);
399 [self.keychainView waitUntilAllOperationsAreFinished];
400 [self waitForCKModifications];
403 - (void)testDeleteItem {
404 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
406 [self startCKKSSubsystem];
408 // We expect a single record to be uploaded.
409 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
410 [self addGenericPassword: @"data" account: @"account-delete-me"];
411 OCMVerifyAllWithDelay(self.mockDatabase, 20);
413 // We expect a single record to be deleted.
414 [self expectCKDeleteItemRecords: 1 zoneID:self.keychainZoneID];
415 [self deleteGenericPassword:@"account-delete-me"];
416 OCMVerifyAllWithDelay(self.mockDatabase, 20);
419 - (void)testDeleteItemImmediatelyAfterModify {
420 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
421 NSString* account = @"account-delete-me";
423 [self startCKKSSubsystem];
425 // We expect a single record to be uploaded.
426 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
427 [self addGenericPassword: @"data" account: account];
428 OCMVerifyAllWithDelay(self.mockDatabase, 20);
430 // Now, hold the modify
431 [self holdCloudKitModifications];
433 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
434 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
435 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
437 [self updateGenericPassword: @"otherdata" account:account];
438 OCMVerifyAllWithDelay(self.mockDatabase, 20);
440 // Right now, the write in CloudKit is pending. Make the local deletion...
441 [self deleteGenericPassword:account];
443 // And then schedule the update
444 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
445 [self releaseCloudKitModificationHold];
447 OCMVerifyAllWithDelay(self.mockDatabase, 20);
450 - (void)testDeleteItemAfterFetchAfterModify {
451 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
452 NSString* account = @"account-delete-me";
454 [self startCKKSSubsystem];
456 // We expect a single record to be uploaded.
457 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
458 [self addGenericPassword: @"data" account: account];
459 OCMVerifyAllWithDelay(self.mockDatabase, 20);
461 // Now, hold the modify
462 //[self holdCloudKitModifications];
464 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
465 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
466 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
468 [self updateGenericPassword: @"otherdata" account:account];
469 OCMVerifyAllWithDelay(self.mockDatabase, 20);
471 // Right now, the write in CloudKit is pending. Place a hold on outgoing queue processing
472 // Place a hold on processing the outgoing queue.
473 CKKSResultOperation* blockOutgoing = [CKKSResultOperation operationWithBlock:^{
474 secnotice("ckks", "Outgoing queue hold released.");
476 blockOutgoing.name = @"outgoing-queue-hold";
477 CKKSResultOperation* outgoingQueueOperation = [self.keychainView processOutgoingQueueAfter:blockOutgoing ckoperationGroup:nil];
479 [self deleteGenericPassword:account];
481 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
483 // Release the CK modification hold
484 //[self releaseCloudKitModificationHold];
487 [self.keychainView waitForFetchAndIncomingQueueProcessing];
488 [self.operationQueue addOperation:blockOutgoing];
489 [outgoingQueueOperation waitUntilFinished];
491 OCMVerifyAllWithDelay(self.mockDatabase, 20);
494 - (void)testDeleteItemWithoutTombstones {
495 // The keychain API allows a client to ask for an inconsistent sync state:
496 // They can ask for a local item deletion without propagating the deletion off-device.
497 // This is the only halfway reasonable way to do keychain item deletions on account signout with the current API
499 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
500 NSString* account = @"account-delete-me";
502 [self startCKKSSubsystem];
503 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"key state should enter 'ready'");
504 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
506 // We expect a single record to be uploaded.
507 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
508 [self addGenericPassword: @"data" account: account];
509 OCMVerifyAllWithDelay(self.mockDatabase, 20);
511 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
513 [self deleteGenericPasswordWithoutTombstones:account];
514 [self findGenericPassword:account expecting:errSecItemNotFound];
516 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
518 // Ensure nothing is in the outgoing queue
519 [self.keychainView dispatchSync:^bool {
520 NSError* error = nil;
521 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID
523 XCTAssertNil(error, "should be no error fetching uuids");
524 XCTAssertEqual(uuids.count, 0u, "There should be zero OQEs");
528 // And a simple fetch doesn't bring it back
529 [self.keychainView waitForFetchAndIncomingQueueProcessing];
530 [self findGenericPassword:account expecting:errSecItemNotFound];
533 CKKSSynchronizeOperation* resyncOperation = [self.keychainView resyncWithCloud];
534 [resyncOperation waitUntilFinished];
535 XCTAssertNil(resyncOperation.error, "No error during the resync operation");
538 [self findGenericPassword:account expecting:errSecSuccess];
540 OCMVerifyAllWithDelay(self.mockDatabase, 20);
544 - (void)testReceiveItem {
545 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
546 [self startCKKSSubsystem];
548 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
549 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
550 (id)kSecAttrAccount : @"account-delete-me",
551 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
552 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
555 CFTypeRef item = NULL;
556 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
558 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
559 [self.keychainZone addToZone: ckr];
561 // Trigger a notification (with hilariously fake data)
562 [self.keychainView notifyZoneChange:nil];
564 [self.keychainView waitForFetchAndIncomingQueueProcessing];
565 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
568 - (void)testReceiveManyItems {
569 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
570 [self startCKKSSubsystem];
572 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D00" withAccount:@"account0"]];
573 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D01" withAccount:@"account1"]];
574 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D02" withAccount:@"account2"]];
575 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D03" withAccount:@"account3"]];
576 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D04" withAccount:@"account4"]];
577 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D05" withAccount:@"account5"]];
578 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D06" withAccount:@"account6"]];
579 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D07" withAccount:@"account7"]];
580 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D08" withAccount:@"account8"]];
581 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D09" withAccount:@"account9"]];
582 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D10" withAccount:@"account10"]];
583 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D11" withAccount:@"account11"]];
585 for(int i = 12; i < 100; i++) {
587 NSString* recordName = [NSString stringWithFormat:@"7B598D31-F9C5-481E-98AC-%012d", i];
588 NSString* account = [NSString stringWithFormat:@"account%d", i];
590 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:recordName withAccount:account]];
594 // Trigger a notification (with hilariously fake data)
595 [self.keychainView notifyZoneChange:nil];
597 [self.keychainView waitForFetchAndIncomingQueueProcessing];
599 [self findGenericPassword: @"account0" expecting:errSecSuccess];
600 [self findGenericPassword: @"account1" expecting:errSecSuccess];
601 [self findGenericPassword: @"account2" expecting:errSecSuccess];
602 [self findGenericPassword: @"account3" expecting:errSecSuccess];
603 [self findGenericPassword: @"account4" expecting:errSecSuccess];
604 [self findGenericPassword: @"account5" expecting:errSecSuccess];
605 [self findGenericPassword: @"account6" expecting:errSecSuccess];
606 [self findGenericPassword: @"account7" expecting:errSecSuccess];
607 [self findGenericPassword: @"account8" expecting:errSecSuccess];
608 [self findGenericPassword: @"account9" expecting:errSecSuccess];
609 [self findGenericPassword: @"account10" expecting:errSecSuccess];
610 [self findGenericPassword: @"account11" expecting:errSecSuccess];
613 - (void)testReceiveCollidingItem {
614 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
615 [self startCKKSSubsystem];
617 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
618 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
619 (id)kSecAttrAccount : @"account-delete-me",
620 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
621 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
624 CFTypeRef item = NULL;
625 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
627 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
628 CKRecord* ckr2 = [self createFakeRecord: self.keychainZoneID recordName: @"F9C58D31-7B59-481E-98AC-5A507ACB2D85"];
630 [self.keychainZone addToZone: ckr];
631 [self.keychainZone addToZone: ckr2];
633 // We expect a delete operation with the "higher" UUID.
634 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
636 // Trigger a notification (with hilariously fake data)
637 [self.keychainView notifyZoneChange:nil];;
639 OCMVerifyAllWithDelay(self.mockDatabase, 20);
640 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
642 [self waitForCKModifications];
643 XCTAssertNil(self.keychainZone.currentDatabase[ckr2.recordID], "Correct record was deleted from CloudKit");
646 -(void)testReceiveItemDelete {
647 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
648 [self startCKKSSubsystem];
650 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
651 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
652 (id)kSecAttrAccount : @"account-delete-me",
653 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
654 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
657 CFTypeRef item = NULL;
658 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
660 [self.keychainView waitForFetchAndIncomingQueueProcessing];
662 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
663 [self.keychainZone addToZone: ckr];
665 // Trigger a notification (with hilariously fake data)
666 [self.keychainView notifyZoneChange:nil];
667 [self.keychainView waitForFetchAndIncomingQueueProcessing];
669 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
673 [self.keychainZone deleteCKRecordIDFromZone: [ckr recordID]];
674 [self.keychainView notifyZoneChange:nil];
675 [self.keychainView waitForFetchAndIncomingQueueProcessing];
677 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
680 -(void)testReceiveItemPhantomDelete {
681 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
682 [self startCKKSSubsystem];
684 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
685 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
686 (id)kSecAttrAccount : @"account-delete-me",
687 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
688 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
691 CFTypeRef item = NULL;
692 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
694 [self.keychainView waitForFetchAndIncomingQueueProcessing];
696 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
697 [self.keychainZone addToZone: ckr];
699 // Trigger a notification (with hilariously fake data)
700 [self.keychainView notifyZoneChange:nil];
701 [self.keychainView waitForFetchAndIncomingQueueProcessing];
703 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
706 [self.keychainView waitUntilAllOperationsAreFinished];
709 [self.keychainZone deleteCKRecordIDFromZone: [ckr recordID]];
711 // and add another, incorrect IQE
712 [self.keychainView dispatchSync: ^bool {
713 // Inefficient, but hey, it works
714 CKRecord* record = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-FFFF-FFFF-5A507ACB2D85"];
715 CKKSItem* fakeItem = [[CKKSItem alloc] initWithCKRecord: record];
717 CKKSIncomingQueueEntry* iqe = [[CKKSIncomingQueueEntry alloc] initWithCKKSItem:fakeItem
718 action:SecCKKSActionDelete
719 state:SecCKKSStateNew];
720 XCTAssertNotNil(iqe, "could create fake IQE");
721 NSError* error = nil;
722 XCTAssert([iqe saveToDatabase: &error], "Saved fake IQE to database");
723 XCTAssertNil(error, "No error saving fake IQE to database");
727 [self.keychainView notifyZoneChange:nil];
728 [self.keychainView waitForFetchAndIncomingQueueProcessing];
730 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
732 // The incoming queue should be empty
733 [self.keychainView dispatchSync: ^bool {
734 NSError* error = nil;
735 NSArray* iqes = [CKKSIncomingQueueEntry all:&error];
736 XCTAssertNil(error, "No error loading IQEs");
737 XCTAssertNotNil(iqes, "Could load IQEs");
738 XCTAssertEqual(iqes.count, 0u, "Incoming queue is empty");
742 -(void)testReceiveConflictOnJustAddedItem {
743 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
744 [self startCKKSSubsystem];
746 [self.keychainView waitForKeyHierarchyReadiness];
747 [self.keychainView waitUntilAllOperationsAreFinished];
749 // Place a hold on processing the outgoing queue.
750 CKKSResultOperation* blockOutgoing = [CKKSResultOperation operationWithBlock:^{
751 secnotice("ckks", "Outgoing queue hold released.");
753 blockOutgoing.name = @"outgoing-queue-hold";
754 CKKSResultOperation* outgoingQueueOperation = [self.keychainView processOutgoingQueueAfter:blockOutgoing ckoperationGroup:nil];
756 CKKSResultOperation* blockIncoming = [CKKSResultOperation operationWithBlock:^{
757 secnotice("ckks", "Incoming queue hold released.");
759 blockIncoming.name = @"incoming-queue-hold";
760 CKKSResultOperation* incomingQueueOperation = [self.keychainView processIncomingQueue:false after: blockIncoming];
762 [self addGenericPassword:@"localchange" account:@"account-delete-me"];
764 // Pull out the new item's UUID.
765 __block NSString* itemUUID = nil;
766 [self.keychainView dispatchSync:^bool {
767 NSError* error = nil;
768 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID ?: [[CKRecordZoneID alloc] initWithZoneName:@"keychain"
769 ownerName:CKCurrentUserDefaultName]
771 XCTAssertNil(error, "no error fetching uuids");
772 XCTAssertEqual(uuids.count, 1u, "There's exactly one outgoing queue entry");
775 XCTAssertNotNil(itemUUID, "Have a UUID for our new item");
779 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName: itemUUID]];
781 [self.keychainView notifyZoneChange:nil];
782 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
784 // Allow the outgoing queue operation to proceed
785 [self.operationQueue addOperation:blockOutgoing];
786 [outgoingQueueOperation waitUntilFinished];
788 // Allow the incoming queue operation to proceed
789 [self.operationQueue addOperation:blockIncoming];
790 [incomingQueueOperation waitUntilFinished];
792 [self checkGenericPassword:@"data" account:@"account-delete-me"];
794 [self.keychainView waitUntilAllOperationsAreFinished];
797 - (void)testReceiveCloudKitConflictOnJustAddedItems {
798 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
799 [self startCKKSSubsystem];
801 [self.keychainView waitForKeyHierarchyReadiness];
802 [self.keychainView waitUntilAllOperationsAreFinished];
804 // Place a hold on processing the outgoing queue.
805 self.keychainView.holdOutgoingQueueOperation = [CKKSResultOperation named:@"outgoing-queue-hold" withBlock:^{
806 secnotice("ckks", "Outgoing queue hold released.");
809 [self addGenericPassword:@"localchange" account:@"account-delete-me"];
811 // Pull out the new item's UUID.
812 __block NSString* itemUUID = nil;
813 [self.keychainView dispatchSync:^bool {
814 NSError* error = nil;
815 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID ?: [[CKRecordZoneID alloc] initWithZoneName:@"keychain"
816 ownerName:CKCurrentUserDefaultName]
818 XCTAssertNil(error, "no error fetching uuids");
819 XCTAssertEqual(uuids.count, 1u, "There's exactly one outgoing queue entry");
822 XCTAssertNotNil(itemUUID, "Have a UUID for our new item");
826 // Add a second item: this item should be uploaded after the failure of the first item
827 [self addGenericPassword:@"localchange" account:@"account-delete-me-2"];
829 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName: itemUUID]];
831 // Also, this write will increment the class C current pointer's etag
832 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
833 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
834 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
835 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
836 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
838 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
839 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
840 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
842 // Allow the outgoing queue operation to proceed
843 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
845 OCMVerifyAllWithDelay(self.mockDatabase, 20);
846 [self.keychainView waitUntilAllOperationsAreFinished];
848 [self checkGenericPassword:@"data" account:@"account-delete-me"];
849 [self checkGenericPassword:@"localchange" account:@"account-delete-me-2"];
853 -(void)testReceiveUnknownField {
854 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
856 [self startCKKSSubsystem];
857 [self.keychainView waitForKeyHierarchyReadiness];
859 NSError* error = nil;
861 // Manually encrypt an item
862 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
863 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
864 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
865 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
866 parentKeyUUID:self.keychainZoneKeys.classA.uuid
867 zoneID:recordID.zoneID];
868 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classA error:&error];
869 XCTAssertNotNil(itemkey, "Got a key");
870 cipheritem.wrappedkey = itemkey.wrappedkey;
871 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
873 NSData* future_data_field = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding];
874 NSString* future_string_field = @"authstring";
875 NSString* future_server_field = @"server_can_change_at_any_time";
876 NSNumber* future_number_field = [NSNumber numberWithInt:30];
878 // Use version 2, so future fields will be authenticated
879 cipheritem.encver = CKKSItemEncryptionVersion2;
880 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
882 authenticatedData[@"future_data_field"] = future_data_field;
883 authenticatedData[@"future_string_field"] = [future_string_field dataUsingEncoding:NSUTF8StringEncoding];
885 uint64_t n = OSSwapHostToLittleConstInt64([future_number_field unsignedLongValue]);
886 authenticatedData[@"future_number_field"] = [NSData dataWithBytes:&n length:sizeof(n)];
889 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
890 XCTAssertNil(error, "no error encrypting object");
891 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
893 CKRecord* ckr = [cipheritem CKRecordWithZoneID: recordID.zoneID];
894 ckr[@"future_data_field"] = future_data_field;
895 ckr[@"future_string_field"] = future_string_field;
896 ckr[@"future_number_field"] = future_number_field;
897 ckr[@"server_new_server_field"] = future_server_field;
898 [self.keychainZone addToZone:ckr];
900 [self.keychainView waitForFetchAndIncomingQueueProcessing];
902 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
903 (id)kSecReturnAttributes: @YES,
904 (id)kSecAttrSynchronizable: @YES,
905 (id)kSecAttrAccount: @"account-delete-me",
906 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
908 CFTypeRef cfresult = NULL;
909 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
911 // Test that if this item is updated, it remains encrypted in v2, and future_field still exists
912 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
913 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
915 OCMVerifyAllWithDelay(self.mockDatabase, 20);
916 [self waitForCKModifications];
918 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
919 XCTAssertEqualObjects(newRecord[@"future_data_field"], future_data_field, "future_data_field still exists");
920 XCTAssertEqualObjects(newRecord[@"future_string_field"], future_string_field, "future_string_field still exists");
921 XCTAssertEqualObjects(newRecord[@"future_number_field"], future_number_field, "future_string_field still exists");
922 XCTAssertEqualObjects(newRecord[@"server_new_server_field"], future_server_field, "future_server_field stille exists");
924 CKKSItem* newItem = [[CKKSItem alloc] initWithCKRecord:newRecord];
925 CKKSAESSIVKey* newItemKey = [self.keychainZoneKeys.classA unwrapAESKey:newItem.wrappedkey error:&error];
926 XCTAssertNil(error, "No error unwrapping AES key");
927 XCTAssertNotNil(newItemKey, "Have an unwrapped AES key for this item");
929 NSDictionary* uploadedData = [CKKSItemEncrypter decryptDictionary:newRecord[SecCKRecordDataKey]
931 authenticatedData:authenticatedData
933 XCTAssertNil(error, "No error decrypting dictionary");
934 XCTAssertNotNil(uploadedData, "Authenticated re-uploaded data including future_field");
935 XCTAssertEqualObjects(uploadedData[@"v_Data"], [@"different password" dataUsingEncoding:NSUTF8StringEncoding], "Passwords match");
939 -(void)testReceiveRecordEncryptedv1 {
940 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
942 [self startCKKSSubsystem];
943 [self.keychainView waitForKeyHierarchyReadiness];
945 NSError* error = nil;
947 // Manually encrypt an item
948 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
949 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
950 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
951 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
952 parentKeyUUID:self.keychainZoneKeys.classC.uuid
953 zoneID:recordID.zoneID];
954 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
955 XCTAssertNotNil(itemkey, "Got a key");
956 cipheritem.wrappedkey = itemkey.wrappedkey;
957 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
959 cipheritem.encver = CKKSItemEncryptionVersion1;
961 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:cipheritem.encver] mutableCopy];
963 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
964 XCTAssertNil(error, "no error encrypting object");
965 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
967 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
969 [self.keychainView waitForFetchAndIncomingQueueProcessing];
971 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
972 (id)kSecReturnAttributes: @YES,
973 (id)kSecAttrSynchronizable: @YES,
974 (id)kSecAttrAccount: @"account-delete-me",
975 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
977 CFTypeRef cfresult = NULL;
978 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
979 CFReleaseNull(cfresult);
981 // Test that if this item is updated, it is encrypted in v2
982 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
983 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
985 OCMVerifyAllWithDelay(self.mockDatabase, 20);
986 [self waitForCKModifications];
988 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
989 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
992 - (void)testUploadPagination {
993 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
995 for(size_t count = 0; count < 250; count++) {
996 [self addGenericPassword: @"data" account: [NSString stringWithFormat:@"account-delete-me-%03lu", count]];
999 [self startCKKSSubsystem];
1001 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1002 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1003 [self expectCKModifyItemRecords: 50 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1005 OCMVerifyAllWithDelay(self.mockDatabase, 40);
1008 - (void)testUploadInitialKeyHierarchy {
1009 // Test starts with nothing in database. CKKS should get into the "please upload my keys" state, then Octagon should perform the upload
1011 // Spin up CKKS subsystem.
1012 [self startCKKSSubsystem];
1014 [self performOctagonTLKUpload:self.ckksViews];
1015 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1017 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
1020 - (void)testDoNotErrorIfNudgedWhileWaitingForTLKUpload {
1021 // Test starts with nothing in database. CKKS should get into the "please upload my keys" state, then Octagon should perform the upload
1023 // Spin up CKKS subsystem.
1024 [self startCKKSSubsystem];
1026 NSMutableArray<CKKSResultOperation<CKKSKeySetProviderOperationProtocol>*>* keysetOps = [NSMutableArray array];
1028 for(CKKSKeychainView* view in self.ckksViews) {
1029 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:40*NSEC_PER_SEC], @"key state should enter 'waitfortlkcreation' (view %@)", view);
1030 [keysetOps addObject: [view findKeySet]];
1033 // Now that we've kicked them all off, wait for them to resolve (and nudge each one, as if a key was saved)
1034 for(CKKSKeychainView* view in self.ckksViews) {
1035 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKUpload] wait:40*NSEC_PER_SEC], @"key state should enter 'waitfortlkupload'");
1037 CKKSCondition* viewProcess = view.keyHierarchyConditions[SecCKKSZoneKeyStateProcess];
1038 [view keyStateMachineRequestProcess];
1039 XCTAssertNotEqual(0, [viewProcess wait:500*NSEC_PER_MSEC], "CKKS should not reprocess the key hierarchy, even if nudged");
1042 // The views should remain in waitfortlkcreation, and not go through process into an error
1044 NSMutableArray<CKRecord*>* keyHierarchyRecords = [NSMutableArray array];
1046 for(CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp in keysetOps) {
1047 // Wait until finished is usually a bad idea. We could rip this out into an operation if we'd like.
1048 [keysetOp waitUntilFinished];
1049 XCTAssertNil(keysetOp.error, "Should be no error fetching keyset from CKKS");
1051 NSArray<CKRecord*>* records = [self putKeySetInCloudKit:keysetOp.keyset];
1052 [keyHierarchyRecords addObjectsFromArray:records];
1055 // Tell our views about our shiny new records!
1056 for(CKKSKeychainView* view in self.ckksViews) {
1057 [view receiveTLKUploadRecords: keyHierarchyRecords];
1059 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1061 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
1064 - (void)testProvideKeysetFromNoTrust {
1066 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1067 [self startCKKSSubsystem];
1069 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlkcreation'");
1070 // I'm not sure how CKKS ends up in 'waitfortrust' without a keyset, so force that state
1071 // In 52301278, it occurred with some complex interaction of zone deletions, fetches, and trust operations
1072 [self.keychainView dispatchSyncWithAccountKeys:^bool{
1073 [self.keychainView _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateWaitForTrust withError:nil];
1076 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortrust'");
1078 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet];
1079 [keysetOp timeout:20*NSEC_PER_SEC];
1080 [keysetOp waitUntilFinished];
1082 XCTAssertNil(keysetOp.error, "Should be no error fetching a keyset");
1085 // This test no longer is very interesting, since Octagon needs to handle lock states, not CKKS...
1086 - (void)testUploadInitialKeyHierarchyAfterLockedStart {
1087 // 'Lock' the keybag
1088 self.aksLockState = true;
1089 [self.lockStateTracker recheck];
1091 [self startCKKSSubsystem];
1093 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1094 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitfortlkcreation");
1096 // After unlock, the key hierarchy should be created.
1097 self.aksLockState = false;
1098 [self.lockStateTracker recheck];
1100 [self performOctagonTLKUpload:self.ckksViews];
1102 // We expect a single class C record to be uploaded.
1103 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1105 [self addGenericPassword: @"data" account: @"account-delete-me"];
1106 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1109 - (void)testLockImmediatelyAfterUploadingInitialKeyHierarchy {
1111 __weak __typeof(self) weakSelf = self;
1113 [self startCKKSSubsystem];
1114 [self performOctagonTLKUpload:self.ckksViews afterUpload:^{
1115 __strong __typeof(self) strongSelf = weakSelf;
1116 [strongSelf holdCloudKitFetches];
1119 // Should enter 'ready'
1120 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1121 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1123 // Now, lock and allow fetches again
1124 self.aksLockState = true;
1125 [self.lockStateTracker recheck];
1126 [self releaseCloudKitFetchHold];
1128 CKKSResultOperation* op = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
1129 [op waitUntilFinished];
1131 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1133 // Wait for CKKS to shake itself out...
1134 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
1136 // Should be in ReadyPendingUnlock
1137 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1139 // We expect a single class C record to be uploaded.
1140 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1141 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1143 [self addGenericPassword: @"data" account: @"account-delete-me"];
1144 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1147 - (void)testReceiveKeyHierarchyAfterLockedStart {
1148 // 'Lock' the keybag
1149 self.aksLockState = true;
1150 [self.lockStateTracker recheck];
1152 [self startCKKSSubsystem];
1154 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1155 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitfortlkcreation");
1157 // Now, another device comes along and creates the hierarchy; we download it; and it and sends us the TLK
1158 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1159 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1160 [self.keychainView notifyZoneChange:nil];
1161 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
1163 self.aksLockState = false;
1164 [self.lockStateTracker recheck];
1165 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should end up in waitfortlk");
1167 // After unlock, the TLK arrives
1168 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1169 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1171 // We expect a single class C record to be uploaded.
1172 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1174 [self addGenericPassword: @"data" account: @"account-delete-me"];
1175 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1178 - (void)testLoadKeyHierarchyAfterLockedStart {
1179 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
1181 // 'Lock' the keybag
1182 self.aksLockState = true;
1183 [self.lockStateTracker recheck];
1185 [self startCKKSSubsystem];
1187 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1188 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1190 self.aksLockState = false;
1191 [self.lockStateTracker recheck];
1193 // We expect a single class C record to be uploaded.
1194 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1196 [self addGenericPassword: @"data" account: @"account-delete-me"];
1197 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1200 - (void)testUploadAndUseKeyHierarchy {
1201 [self startCKKSSubsystem];
1202 [self performOctagonTLKUpload:self.ckksViews];
1204 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
1205 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1206 (id)kSecAttrAccount : @"account-delete-me",
1207 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1208 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1210 CFTypeRef item = NULL;
1211 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not exist");
1213 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1214 [self waitForCKModifications];
1216 // We expect a single class C record to be uploaded.
1217 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1219 [self addGenericPassword: @"data" account: @"account-delete-me"];
1220 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1222 // now, expect a single class A record to be uploaded
1223 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1225 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef)@{
1226 (id)kSecClass : (id)kSecClassGenericPassword,
1227 (id)kSecAttrAccessGroup : @"com.apple.security.sos",
1228 (id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked,
1229 (id)kSecAttrAccount : @"account-class-A",
1230 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1231 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
1232 }, NULL), @"Adding class A item");
1233 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1236 - (void)testUploadInitialKeyHierarchyTriggersBackup {
1237 // We also expect the view manager's notifyNewTLKsInKeychain call to fire (after some delay)
1238 OCMExpect([self.mockCKKSViewManager notifyNewTLKsInKeychain]);
1240 // Spin up CKKS subsystem.
1241 [self startCKKSSubsystem];
1242 [self performOctagonTLKUpload:self.ckksViews];
1244 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1245 OCMVerifyAllWithDelay(self.mockCKKSViewManager, 10);
1248 - (void)testResetCloudKitZoneFromNoTLK {
1249 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1250 OCMExpect([self.suggestTLKUpload trigger]);
1252 self.silentZoneDeletesAllowed = true;
1254 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1255 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1256 // explicitly do not save a fake device status here
1257 self.keychainZone.flag = true;
1259 [self startCKKSSubsystem];
1260 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1262 // But then, it'll fire off the reset and reach 'ready', with a little help from octagon
1263 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1264 [self performOctagonTLKUpload:self.ckksViews];
1266 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1267 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1269 // And the zone should have been cleared and re-made
1270 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1273 - (void)testResetCloudKitZoneFromNoTLKWithOtherWaitForTLKDevices {
1274 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1275 OCMExpect([self.suggestTLKUpload trigger]);
1277 self.silentZoneDeletesAllowed = true;
1279 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1280 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1281 // Save a fake device status here, but modify its key state to be 'waitfortlk': it has no idea what the TLK is either
1282 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1283 [self putFakeOctagonOnlyDeviceStatusInCloudKit:self.keychainZoneID];
1285 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1286 if([record.recordType isEqualToString:SecCKRecordDeviceStateType]) {
1287 record[SecCKRecordKeyState] = CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTLK);
1291 self.keychainZone.flag = true;
1293 [self startCKKSSubsystem];
1294 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1296 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1297 [self performOctagonTLKUpload:self.ckksViews];
1299 // But then, it'll fire off the reset and reach 'ready'
1300 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1301 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1303 // And the zone should have been cleared and re-made
1304 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1307 - (void)testResetCloudKitZoneFromNoTLKIgnoringInactiveDevices {
1308 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1309 OCMExpect([self.suggestTLKUpload trigger]);
1311 self.silentZoneDeletesAllowed = true;
1313 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1314 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1315 // Save a fake device status here, but modify its creation and modification times to be months ago
1316 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1317 [self putFakeOctagonOnlyDeviceStatusInCloudKit:self.keychainZoneID];
1319 // Put a 'in-circle' TLKShare record, but also modify its creation and modification times
1320 CKKSSOSSelfPeer* untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"untrusted-peer"
1321 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1322 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1323 viewList:self.managedViewList];
1324 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:untrustedPeer to:untrustedPeer zoneID:self.keychainZoneID];
1326 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1327 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1328 record.creationDate = [NSDate distantPast];
1329 record.modificationDate = [NSDate distantPast];
1333 self.keychainZone.flag = true;
1335 [self startCKKSSubsystem];
1336 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1338 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1339 [self performOctagonTLKUpload:self.ckksViews];
1341 // But then, it'll fire off the reset and reach 'ready'
1342 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1343 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1345 // And the zone should have been cleared and re-made
1346 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1349 - (void)testDoNotResetCloudKitZoneDuringBadCircleState {
1350 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1351 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1353 // This test has stuff in CloudKit, but no TLKs.
1354 // CKKS should NOT reset the CK zone.
1355 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1356 self.zones[self.keychainZoneID].flag = true;
1358 [self startCKKSSubsystem];
1360 // But since we're out of circle, this test needs to initialize the zone itself
1361 [self.keychainView beginCloudKitOperation];
1363 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered waitfortrust");
1364 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1366 FakeCKZone* keychainZone = self.zones[self.keychainZoneID];
1367 XCTAssertNotNil(keychainZone, "Should still have a keychain zone");
1368 XCTAssertTrue(keychainZone.flag, "keychain zone should not have been recreated");
1371 - (void)testDoNotResetCloudKitZoneFromWaitForTLKDueToRecentDeviceState {
1372 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1374 // CKKS shouldn't reset this zone, due to a recent device status claiming to have TLKs
1375 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1377 // Also, CKKS _should_ be able to return the key hierarchy if asked before it starts
1378 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet];
1380 NSDateComponents* offset = [[NSDateComponents alloc] init];
1382 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1383 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1384 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1385 record.creationDate = updateTime;
1386 record.modificationDate = updateTime;
1390 self.keychainZone.flag = true;
1391 [self startCKKSSubsystem];
1393 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1395 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1397 // And, ensure that the keyset op ran and has results
1398 CKKSResultOperation* waitOp = [CKKSResultOperation named:@"test op" withBlock:^{}];
1399 [waitOp addDependency:keysetOp];
1400 [waitOp timeout:2*NSEC_PER_SEC];
1401 [self.operationQueue addOperation:waitOp];
1402 [waitOp waitUntilFinished];
1404 XCTAssert(keysetOp.finished, "Keyset op should have finished");
1405 XCTAssertNil(keysetOp.error, "keyset op should not have errored");
1406 XCTAssertNotNil(keysetOp.keyset, "keyset op should have a keyset");
1407 XCTAssertNotNil(keysetOp.keyset.currentTLKPointer, "keyset should have a current TLK pointer");
1408 XCTAssertEqualObjects(keysetOp.keyset.currentTLKPointer.currentKeyUUID, self.keychainZoneKeys.tlk.uuid, "keyset should match what's in zone");
1411 - (void)testDoNotCloudKitZoneFromWaitForTLKDueToRecentButUntrustedDeviceState {
1412 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1414 // CKKS should reset this zone, even though to a recent device status claiming to have TLKs. The device isn't trusted
1415 self.silentZoneDeletesAllowed = true;
1416 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1417 [self.mockSOSAdapter.trustedPeers removeObject:self.remoteSOSOnlyPeer];
1419 self.keychainZone.flag = true;
1420 [self startCKKSSubsystem];
1422 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1423 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1425 // And ensure it doesn't go on to 'reset'
1426 XCTAssertNotEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:100*NSEC_PER_MSEC], @"Key state should not become 'resetzone'");
1429 - (void)testResetCloudKitZoneFromWaitForTLKDueToLessRecentAndUntrustedDeviceState {
1430 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1431 OCMExpect([self.suggestTLKUpload trigger]);
1433 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1435 // CKKS should reset this zone, even though to a recent device status claiming to have TLKs. The device isn't trusted
1436 self.silentZoneDeletesAllowed = true;
1437 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1438 [self.mockSOSAdapter.trustedPeers removeObject:self.remoteSOSOnlyPeer];
1440 NSDateComponents* offset = [[NSDateComponents alloc] init];
1442 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1443 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1444 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1445 record.creationDate = updateTime;
1446 record.modificationDate = updateTime;
1450 self.keychainZone.flag = true;
1451 [self startCKKSSubsystem];
1452 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1454 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1455 [self performOctagonTLKUpload:self.ckksViews];
1457 // Then we should reset.
1458 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1459 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1461 // And the zone should have been cleared and re-made
1462 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1465 - (void)testAcceptExistingKeyHierarchy {
1466 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
1467 // Test also begins with the TLK having arrived in the local keychain (via SOS)
1468 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1469 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1470 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1472 // Spin up CKKS subsystem.
1473 [self startCKKSSubsystem];
1475 // The CKKS subsystem should only upload its TLK share
1476 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1478 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1480 // Verify that there are three local keys, and three local current key records
1481 __weak __typeof(self) weakSelf = self;
1482 [self.keychainView dispatchSync: ^bool{
1483 __strong __typeof(weakSelf) strongSelf = weakSelf;
1484 XCTAssertNotNil(strongSelf, "self exists");
1486 NSError* error = nil;
1488 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
1489 XCTAssertNil(error, "no error fetching keys");
1490 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
1492 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1493 XCTAssertNil(error, "no error fetching current keys");
1494 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1500 - (void)testAcceptExistingAndUseKeyHierarchy {
1501 // Test starts with nothing in database, but one in our fake CloudKit.
1502 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1503 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1504 // But, CKKS shouldn't ever reset the zone
1505 self.keychainZone.flag = true;
1507 [self startCKKSSubsystem];
1508 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:200*NSEC_PER_SEC], "Key state should have become waitfortlk");
1510 // Now, save the TLK to the keychain (to simulate it coming in later via SOS). We'll create a TLK share for ourselves.
1511 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1512 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1514 // Wait for the key hierarchy to sort itself out, to make it easier on this test; see testOnboardOldItemsWithExistingKeyHierarchy for the other test.
1515 // The CKKS subsystem should write its TLK share, but nothing else
1516 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1518 // We expect a single record to be uploaded for each key class
1519 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1520 [self addGenericPassword: @"data" account: @"account-delete-me"];
1521 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1523 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1524 [self addGenericPassword:@"asdf"
1525 account:@"account-class-A"
1527 access:(id)kSecAttrAccessibleWhenUnlocked
1528 expecting:errSecSuccess
1529 message:@"Adding class A item"];
1530 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1531 XCTAssertTrue(self.keychainZone.flag, "Keychain zone shouldn't have been reset");
1534 - (void)testAcceptExistingKeyHierarchyDespiteLocked {
1535 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
1536 // Test also begins with the TLK having arrived in the local keychain (via SOS)
1538 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1539 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1541 self.aksLockState = true;
1542 [self.lockStateTracker recheck];
1544 // Spin up CKKS subsystem.
1545 [self startCKKSSubsystem];
1547 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:20*NSEC_PER_SEC], "Key state should have become waitforunlock");
1549 // CKKS will give itself a TLK Share
1550 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1552 // Now that all operations are complete, 'unlock' AKS
1553 self.aksLockState = false;
1554 [self.lockStateTracker recheck];
1556 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1557 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1559 // Verify that there are three local keys, and three local current key records
1560 __weak __typeof(self) weakSelf = self;
1561 [self.keychainView dispatchSync: ^bool{
1562 __strong __typeof(weakSelf) strongSelf = weakSelf;
1563 XCTAssertNotNil(strongSelf, "self exists");
1565 NSError* error = nil;
1567 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
1568 XCTAssertNil(error, "no error fetching keys");
1569 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
1571 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1572 XCTAssertNil(error, "no error fetching current keys");
1573 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1579 - (void)testReceiveClassCWhileALocked {
1580 // Test starts with a key hierarchy already existing.
1581 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
1582 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1583 [self startCKKSSubsystem];
1585 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1586 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1588 [self findGenericPassword:@"classCItem" expecting:errSecItemNotFound];
1589 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
1591 // 'Lock' the keybag
1592 self.aksLockState = true;
1593 [self.lockStateTracker recheck];
1595 XCTAssertNotNil(self.keychainZoneKeys, "Have zone keys for zone");
1596 XCTAssertNotNil(self.keychainZoneKeys.classA, "Have class A key for zone");
1597 XCTAssertNotNil(self.keychainZoneKeys.classC, "Have class C key for zone");
1599 [self.keychainView dispatchSyncWithAccountKeys: ^bool {
1600 [self.keychainView _onqueueKeyStateMachineRequestProcess];
1603 // And ensure we end up back in 'readypendingunlock': we have the keys, we're just locked now
1604 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
1605 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1607 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"classCItem" key:self.keychainZoneKeys.classC]];
1608 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-FFFF-FFFF-FFFF-5A507ACB2D85" withAccount:@"classAItem" key:self.keychainZoneKeys.classA]];
1610 CKKSResultOperation* op = [self.keychainView waitForFetchAndIncomingQueueProcessing];
1611 // The processing op should NOT error, even though it didn't manage to process the classA item
1612 XCTAssertNil(op.error, "no error while failing to process a class A item");
1614 CKKSResultOperation* erroringOp = [self.keychainView processIncomingQueue:true];
1615 [erroringOp waitUntilFinished];
1616 XCTAssertNotNil(erroringOp.error, "error exists while processing a class A item");
1618 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
1619 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
1621 self.aksLockState = false;
1622 [self.lockStateTracker recheck];
1623 [self.keychainView waitUntilAllOperationsAreFinished];
1625 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
1626 [self findGenericPassword:@"classAItem" expecting:errSecSuccess];
1629 - (void)testRestartWhileLocked {
1630 [self startCKKSSubsystem];
1631 [self performOctagonTLKUpload:self.ckksViews];
1633 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1635 // 'Lock' the keybag
1636 self.aksLockState = true;
1637 [self.lockStateTracker recheck];
1639 [self.keychainView halt];
1640 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1641 [self.keychainView beginCloudKitOperation];
1642 [self beginSOSTrustedViewOperation:self.keychainView];
1644 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1646 self.aksLockState = false;
1647 [self.lockStateTracker recheck];
1649 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1652 - (void)testExternalKeyRoll {
1653 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1654 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1655 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1656 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1658 // Spin up CKKS subsystem.
1659 [self startCKKSSubsystem];
1661 // The CKKS subsystem should not try to write anything to the CloudKit database.
1662 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1664 __weak __typeof(self) weakSelf = self;
1666 // We expect a single record to be uploaded.
1667 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1669 [self addGenericPassword: @"data" account: @"account-delete-me"];
1671 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1672 [self waitForCKModifications];
1674 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1675 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1676 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1678 // Trigger a notification
1679 [self.keychainView notifyZoneChange:nil];
1681 // Make life easy on this test; testAcceptKeyConflictAndUploadReencryptedItem will check the case when we don't receive the notification
1682 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1684 // Just in extra case of threading issues, force a reexamination of the key hierarchy
1685 [self.keychainView dispatchSyncWithAccountKeys: ^bool {
1686 [self.keychainView _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
1690 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1692 // Verify that there are six local keys, and three local current key records
1693 [self.keychainView dispatchSync: ^bool{
1694 __strong __typeof(weakSelf) strongSelf = weakSelf;
1695 XCTAssertNotNil(strongSelf, "self exists");
1697 NSError* error = nil;
1698 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:self.keychainZoneID error:&error];
1699 XCTAssertNil(error, "no error fetching keys");
1700 XCTAssertEqual(keys.count, 6u, "Six keys in local database");
1702 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1703 XCTAssertNil(error, "no error fetching current keys");
1704 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1706 for(CKKSCurrentKeyPointer* key in currentkeys) {
1707 if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
1708 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.tlk.uuid);
1709 } else if([key.keyclass isEqualToString: SecCKKSKeyClassA]) {
1710 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classA.uuid);
1711 } else if([key.keyclass isEqualToString: SecCKKSKeyClassC]) {
1712 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classC.uuid);
1714 XCTFail("Unknown key class: %@", key.keyclass);
1721 // We expect a single record to be uploaded.
1722 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1724 // TODO: remove this by writing code for item reencrypt after key arrival
1725 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1727 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1729 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1732 - (void)testAcceptKeyConflictAndUploadReencryptedItem {
1733 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1734 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1735 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1736 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1738 [self startCKKSSubsystem];
1739 [self.keychainView waitUntilAllOperationsAreFinished];
1741 // We expect a single record to be uploaded.
1742 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1744 [self addGenericPassword: @"data" account: @"account-delete-me"];
1746 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1747 [self waitForCKModifications];
1749 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1751 // Do not trigger a notification here. This should cause a conflict updating the current key records
1753 // We expect a single record to be uploaded, but that the write will be rejected
1754 // We then expect that item to be reuploaded with the current key
1756 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
1757 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1758 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1760 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under rolled class C key in hierarchy"]];
1762 // New key arrives via SOS!
1763 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1764 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1766 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1769 - (void)testAcceptKeyConflictAndUploadReencryptedItems {
1770 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1771 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1772 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1773 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1775 [self startCKKSSubsystem];
1776 [self.keychainView waitUntilAllOperationsAreFinished];
1778 // We expect a single record to be uploaded.
1779 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1780 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1782 [self addGenericPassword: @"data" account: @"account-delete-me"];
1784 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1785 [self waitForCKModifications];
1787 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1789 // Do not trigger a notification here. This should cause a conflict updating the current key records
1791 // We expect a single record to be uploaded, but that the write will be rejected
1792 // We then expect that item to be reuploaded with the current key
1794 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
1795 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1796 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key-2"];
1797 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1799 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1800 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under rolled class C key in hierarchy"]];
1802 // New key arrives via SOS!
1803 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1804 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1806 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1809 - (void)testRecoverFromRequestKeyRefetchWithoutRolling {
1810 // Simply requesting a key state refetch shouldn't roll the key hierarchy.
1812 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1814 // Spin up CKKS subsystem.
1815 [self startCKKSSubsystem];
1817 // Items should upload.
1818 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1819 [self addGenericPassword: @"data" account: @"account-delete-me"];
1820 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1822 [self waitForCKModifications];
1825 // CKKS should not roll the keys while progressing back to 'ready', but it will fetch once
1826 self.silentFetchesAllowed = false;
1827 [self expectCKFetch];
1829 [self.keychainView dispatchSyncWithAccountKeys: ^bool {
1830 [self.keychainView _onqueueKeyStateMachineRequestFetch];
1834 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
1835 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1838 - (void)testRecoverFromIncrementedCurrentKeyPointerEtag {
1839 // CloudKit sometimes reports the current key pointers have changed (etag mismatch), but their content hasn't.
1840 // In this case, CKKS shouldn't roll the TLK.
1842 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1844 // Spin up CKKS subsystem.
1845 [self startCKKSSubsystem];
1846 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // just to be sure it's fetched
1848 // Items should upload.
1849 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1850 [self addGenericPassword: @"data" account: @"account-delete-me"];
1851 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1853 [self waitForCKModifications];
1855 // Bump the etag on the class C current key record, but don't change any data
1856 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
1857 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
1858 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
1860 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
1861 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
1863 // Add another item. This write should fail, then CKKS should recover without rolling the key hierarchy or issuing a fetch.
1864 self.silentFetchesAllowed = false;
1865 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1866 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1867 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
1868 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1871 - (void)testRecoverMultipleItemsFromIncrementedCurrentKeyPointerEtag {
1872 // CloudKit sometimes reports the current key pointers have changed (etag mismatch), but their content hasn't.
1873 // In this case, CKKS shouldn't roll the TLK.
1874 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1876 // Spin up CKKS subsystem.
1877 [self startCKKSSubsystem];
1878 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // just to be sure it's fetched
1880 // Items should upload.
1881 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1882 [self addGenericPassword: @"data" account: @"account-delete-me"];
1883 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1885 [self waitForCKModifications];
1887 // Bump the etag on the class C current key record, but don't change any data
1888 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
1889 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
1890 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
1892 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
1893 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
1895 // Add another item. This write should fail, then CKKS should recover without rolling the key hierarchy or issuing a fetch.
1896 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
1897 secnotice("ckks", "releasing outgoing-queue hold");
1900 self.silentFetchesAllowed = false;
1901 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1902 [self expectCKModifyItemRecords:2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1903 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
1904 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
1906 [self.operationQueue addOperation: self.keychainView.holdOutgoingQueueOperation];
1907 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1910 - (void)testOnboardOldItemsCreatingKeyHierarchy {
1911 // 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
1913 // Test starts with nothing in CloudKit, and CKKS blocked. Add one item without a UUID...
1915 SecCKKSTestSetDisableAutomaticUUID(true);
1916 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1918 // and an item with a UUID...
1919 SecCKKSTestSetDisableAutomaticUUID(false);
1920 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1922 // We then expect an upload of the added items
1923 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1925 [self startCKKSSubsystem];
1926 [self performOctagonTLKUpload:self.ckksViews];
1928 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1931 - (void)testOnboardOldItemsWithExistingKeyHierarchy {
1932 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1934 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1935 [self addGenericPassword: @"data" account: @"account-delete-me"];
1937 [self startCKKSSubsystem];
1938 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1941 - (void)testOnboardOldItemsWithExistingKeyHierarchyExtantTLK {
1942 // Test starts key hierarchy in our fake CloudKit, the TLK arrived in the local keychain, and CKKS blocked.
1943 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1944 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1945 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1947 // Add one item without a UUID...
1948 SecCKKSTestSetDisableAutomaticUUID(true);
1949 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1951 // and an item with a UUID...
1952 SecCKKSTestSetDisableAutomaticUUID(false);
1953 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1955 // 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
1956 // We expect a single record to be uploaded.
1957 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1959 // Spin up CKKS subsystem.
1960 [self startCKKSSubsystem];
1962 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1965 - (void)testOnboardOldItemsWithExistingKeyHierarchyLateTLK {
1966 // Test starts key hierarchy in our fake CloudKit, and CKKS blocked.
1967 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1968 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1969 self.keychainZone.flag = true;
1971 // Add one item without a UUID...
1972 SecCKKSTestSetDisableAutomaticUUID(true);
1973 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1975 // and an item with a UUID...
1976 SecCKKSTestSetDisableAutomaticUUID(false);
1977 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1979 // 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
1981 // Spin up CKKS subsystem.
1982 [self startCKKSSubsystem];
1983 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should have become waitfortlk");
1985 // Now, save the TLK to the keychain (to simulate it coming in via SOS).
1986 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1987 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1989 // We expect a single record to be uploaded.
1990 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1992 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1993 XCTAssertTrue(self.keychainZone.flag, "Keychain zone shouldn't have been reset");
1996 - (void)testResync {
1997 // We need to set up a desynced situation to test our resync.
1998 // First, let CKKS start up and send several items to CloudKit (that we'll then desync!)
1999 __block NSError* error = nil;
2001 // Test starts with keys in CloudKit (so we can create items later)
2002 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2003 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2004 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2006 [self addGenericPassword: @"data" account: @"first"];
2007 [self addGenericPassword: @"data" account: @"second"];
2008 [self addGenericPassword: @"data" account: @"third"];
2009 [self addGenericPassword: @"data" account: @"fourth"];
2010 [self addGenericPassword: @"data" account: @"fifth"];
2011 NSUInteger passwordCount = 5u;
2013 [self checkGenericPassword: @"data" account: @"first"];
2014 [self checkGenericPassword: @"data" account: @"second"];
2015 [self checkGenericPassword: @"data" account: @"third"];
2016 [self checkGenericPassword: @"data" account: @"fourth"];
2017 [self checkGenericPassword: @"data" account: @"fifth"];
2019 [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2021 [self startCKKSSubsystem];
2023 // Wait for uploads to happen
2024 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2025 [self waitForCKModifications];
2026 // One TLK share record
2027 XCTAssertEqual(self.keychainZone.currentDatabase.count, SYSTEM_DB_RECORD_COUNT+passwordCount+1, "Have SYSTEM_DB_RECORD_COUNT+passwordCount+1 objects in cloudkit");
2029 // Now, corrupt away!
2030 // Extract all passwordCount items for Corruption
2031 NSArray<CKRecord*>* items = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
2032 XCTAssertEqual(items.count, passwordCount, "Have %lu Items in cloudkit", (unsigned long)passwordCount);
2034 // For the first record, delete all traces of it from CKKS. But! it remains in local keychain.
2035 // Expected outcome: CKKS resyncs; item exists again.
2036 CKRecord* delete = items[0];
2037 NSString* deleteAccount = [[self decryptRecord: delete] objectForKey: (__bridge id) kSecAttrAccount];
2038 XCTAssertNotNil(deleteAccount, "received an account for the local delete object");
2040 __weak __typeof(self) weakSelf = self;
2041 [self.keychainView dispatchSync:^bool{
2042 __strong __typeof(weakSelf) strongSelf = weakSelf;
2043 XCTAssertNotNil(strongSelf, "self exists");
2045 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2047 [ckme deleteFromDatabase: &error];
2049 XCTAssertNil(error, "no error removing CKME");
2050 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2052 [oqe deleteFromDatabase: &error];
2054 XCTAssertNil(error, "no error removing OQE");
2055 CKKSIncomingQueueEntry* iqe = [CKKSIncomingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2057 [iqe deleteFromDatabase: &error];
2059 XCTAssertNil(error, "no error removing IQE");
2063 // For the second record, delete all traces of it from CloudKit.
2064 // Expected outcome: deleted locally
2065 CKRecord* remoteDelete = items[1];
2066 NSString* remoteDeleteAccount = [[self decryptRecord: remoteDelete] objectForKey: (__bridge id) kSecAttrAccount];
2067 XCTAssertNotNil(remoteDeleteAccount, "received an account for the remote delete object");
2069 [self.keychainZone deleteCKRecordIDFromZone: remoteDelete.recordID];
2070 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
2071 [database removeObjectForKey: remoteDelete.recordID];
2074 // The third record gets modified in CloudKit, but not locally.
2075 // Expected outcome: use the CloudKit version
2076 CKRecord* remoteDataChanged = items[2];
2077 NSMutableDictionary* remoteDataDictionary = [[self decryptRecord: remoteDataChanged] mutableCopy];
2078 NSString* remoteDataChangedAccount = [remoteDataDictionary objectForKey: (__bridge id) kSecAttrAccount];
2079 XCTAssertNotNil(remoteDataChangedAccount, "Received an account for the remote-data-changed object");
2080 remoteDataDictionary[(__bridge id) kSecValueData] = [@"CloudKitWins" dataUsingEncoding: NSUTF8StringEncoding];
2082 CKRecord* newData = [self newRecord: remoteDataChanged.recordID withNewItemData: remoteDataDictionary];
2083 [self.keychainZone addToZone: newData];
2084 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
2085 database[remoteDataChanged.recordID] = newData;
2088 // The fourth record stays in-sync. Good work, everyone!
2089 // Expected outcome: stays in-sync
2090 NSString* insyncAccount = [[self decryptRecord: items[3]] objectForKey: (__bridge id) kSecAttrAccount];
2091 XCTAssertNotNil(insyncAccount, "Received an account for the in-sync object");
2093 // The fifth record is updated locally, but CKKS didn't get the notification, and so the local CKMirror and CloudKit don't have it
2094 // Expected outcome: local change should be steamrolled by the cloud version
2095 CKRecord* localDataChanged = items[4];
2096 NSMutableDictionary* localDataDictionary = [[self decryptRecord: localDataChanged] mutableCopy];
2097 NSString* localDataChangedAccount = [localDataDictionary objectForKey: (__bridge id) kSecAttrAccount];
2099 [self updateGenericPassword:@"newpassword" account:localDataChangedAccount];
2100 [self checkGenericPassword:@"newpassword" account:localDataChangedAccount];
2103 // To make this more challenging, CK returns the refetch in multiple batches. This shouldn't affect the resync...
2104 CKServerChangeToken* ck1 = self.keychainZone.currentChangeToken;
2105 self.silentFetchesAllowed = false;
2106 [self expectCKFetch];
2107 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
2108 // Assert that the fetch is happening with the change token we paused at before
2109 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.keychainZoneID].previousServerChangeToken;
2110 if(changeToken && [changeToken isEqual:ck1]) {
2115 } runBeforeFinished:^{}];
2117 self.keychainZone.limitFetchTo = ck1;
2118 self.keychainZone.limitFetchError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkFailure userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:4]}];
2120 // The sixth record gets magically added to CloudKit, but CKKS has never heard of it
2121 // (emulates a lost record on the client, but that CloudKit already believes it's sent the record for)
2122 // Expected outcome: added to local keychain
2123 NSString* remoteOnlyAccount = @"remote-only";
2124 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount: remoteOnlyAccount];
2125 [self.keychainZone addToZone: ckr];
2126 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
2127 database[ckr.recordID] = ckr;
2130 ckksnotice("ckksresync", self.keychainView, "local delete: %@ %@", delete.recordID.recordName, deleteAccount);
2131 ckksnotice("ckksresync", self.keychainView, "Remote deletion: %@ %@", remoteDelete.recordID.recordName, remoteDeleteAccount);
2132 ckksnotice("ckksresync", self.keychainView, "Remote data changed: %@ %@", remoteDataChanged.recordID.recordName, remoteDataChangedAccount);
2133 ckksnotice("ckksresync", self.keychainView, "in-sync: %@ %@", items[3].recordID.recordName, insyncAccount);
2134 ckksnotice("ckksresync", self.keychainView, "local update: %@ %@", items[4].recordID.recordName, localDataChangedAccount);
2135 ckksnotice("ckksresync", self.keychainView, "Remote only: %@ %@", ckr.recordID.recordName, remoteOnlyAccount);
2137 CKKSSynchronizeOperation* resyncOperation = [self.keychainView resyncWithCloud];
2138 [resyncOperation waitUntilFinished];
2140 XCTAssertNil(resyncOperation.error, "No error during the resync operation");
2142 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2144 // Now do some checking. Remember, we don't know which record we corrupted, so use the parsed account variables to check.
2146 [self findGenericPassword: deleteAccount expecting: errSecSuccess];
2147 [self findGenericPassword: remoteDeleteAccount expecting: errSecItemNotFound];
2148 [self findGenericPassword: remoteDataChangedAccount expecting: errSecSuccess];
2149 [self findGenericPassword: insyncAccount expecting: errSecSuccess];
2150 [self findGenericPassword: localDataChangedAccount expecting: errSecSuccess];
2151 [self findGenericPassword: remoteOnlyAccount expecting: errSecSuccess];
2153 [self checkGenericPassword: @"data" account: deleteAccount];
2154 //[self checkGenericPassword: @"data" account: remoteDeleteAccount];
2155 [self checkGenericPassword: @"CloudKitWins" account: remoteDataChangedAccount];
2156 [self checkGenericPassword: @"data" account: insyncAccount];
2157 [self checkGenericPassword:@"data" account:localDataChangedAccount];
2158 [self checkGenericPassword: @"data" account: remoteOnlyAccount];
2160 [self.keychainView dispatchSync:^bool{
2161 __strong __typeof(weakSelf) strongSelf = weakSelf;
2162 XCTAssertNotNil(strongSelf, "self exists");
2164 CKKSMirrorEntry* ckme = nil;
2166 ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2167 XCTAssertNil(error);
2168 XCTAssertNotNil(ckme);
2170 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDelete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2171 XCTAssertNil(error);
2172 XCTAssertNil(ckme); // deleted!
2174 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDataChanged.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2175 XCTAssertNil(error);
2176 XCTAssertNotNil(ckme);
2178 ckme = [CKKSMirrorEntry tryFromDatabase:items[3].recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2179 XCTAssertNil(error);
2180 XCTAssertNotNil(ckme);
2182 ckme = [CKKSMirrorEntry tryFromDatabase:items[4].recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2183 XCTAssertNil(error);
2184 XCTAssertNotNil(ckme);
2186 ckme = [CKKSMirrorEntry tryFromDatabase:ckr.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2187 XCTAssertNil(error);
2188 XCTAssertNotNil(ckme);
2193 - (void)testResyncItemsMissingFromLocalKeychain {
2194 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2197 // one password correctly synced between local keychain and CloudKit
2198 // one password incorrectly disappeared from local keychain, but in mirror table
2199 // one password sitting in the outgoing queue
2200 // one password sitting in the incoming queue
2202 // Add and sync two passwords
2203 [self addGenericPassword: @"data" account: @"first"];
2204 [self addGenericPassword: @"data" account: @"second"];
2206 [self checkGenericPassword: @"data" account: @"first"];
2207 [self checkGenericPassword: @"data" account: @"second"];
2209 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2210 [self startCKKSSubsystem];
2211 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2212 [self waitForCKModifications];
2213 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2215 // Now, place an item in the outgoing queue
2217 //[self addGenericPassword: @"data" account: @"third"];
2218 //[self checkGenericPassword: @"data" account: @"third"];
2220 // Now, corrupt away!
2221 // Extract all passwordCount items for Corruption
2222 NSArray<CKRecord*>* items = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
2223 XCTAssertEqual(items.count, 2u, "Have %lu Items in cloudkit", (unsigned long)2u);
2225 // For the first record, surreptitiously remove from local keychain
2226 CKRecord* remove = items[0];
2227 NSString* removeAccount = [[self decryptRecord:remove] objectForKey:(__bridge id)kSecAttrAccount];
2228 XCTAssertNotNil(removeAccount, "received an account for the local delete object");
2230 NSURL* kcpath = (__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)@"keychain-2-debug.db");
2232 sqlite3_open([[kcpath path] UTF8String], &db);
2233 NSString* query = [NSString stringWithFormat:@"DELETE FROM genp WHERE uuid=\"%@\"", remove.recordID.recordName];
2234 char* sqlerror = NULL;
2235 XCTAssertEqual(SQLITE_OK, sqlite3_exec(db, [query UTF8String], NULL, NULL, &sqlerror), "SQL deletion shouldn't error");
2236 XCTAssertTrue(sqlerror == NULL, "No error string should have been returned: %s", sqlerror);
2238 sqlite3_free(sqlerror);
2243 // The second record is kept in-sync
2245 // Now, add an in-flight change (for record 3)
2246 [self holdCloudKitModifications];
2247 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2248 [self addGenericPassword:@"data" account:@"third"];
2249 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2251 // For the fourth, add a new record but prevent incoming queue processing
2252 self.keychainView.holdIncomingQueueOperation = [CKKSResultOperation named:@"hold-incoming" withBlock:^{}];
2254 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"fourth"];
2255 [self.keychainZone addToZone:ckr];
2256 [self.keychainView notifyZoneChange:nil];
2258 // Now, where are we....
2259 CKKSScanLocalItemsOperation* scanLocal = [self.keychainView scanLocalItems:@"test-scan"];
2260 [scanLocal waitUntilFinished];
2262 XCTAssertEqual(scanLocal.missingLocalItemsFound, 1u, "Should have found one missing item");
2264 // Allow everything to proceed
2265 [self releaseCloudKitModificationHold];
2266 [self.operationQueue addOperation:self.keychainView.holdIncomingQueueOperation];
2268 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2269 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2271 // And ensure that all four items are present again
2272 [self findGenericPassword: @"first" expecting: errSecSuccess];
2273 [self findGenericPassword: @"second" expecting: errSecSuccess];
2274 [self findGenericPassword: @"third" expecting: errSecSuccess];
2275 [self findGenericPassword: @"fourth" expecting: errSecSuccess];
2278 - (void)testScanItemsChangedInLocalKeychain {
2279 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2281 // Add and sync two passwords
2282 NSString* itemAccount = @"first";
2283 [self addGenericPassword:@"data" account:itemAccount];
2284 [self addGenericPassword:@"data" account:@"second"];
2286 [self checkGenericPassword:@"data" account:itemAccount];
2287 [self checkGenericPassword:@"data" account:@"second"];
2289 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2290 [self startCKKSSubsystem];
2291 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2292 [self waitForCKModifications];
2293 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2295 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
2296 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
2298 // Now, have CKKS miss an update
2300 [self updateGenericPassword:@"newpassword" account:itemAccount];
2301 [self checkGenericPassword:@"newpassword" account:itemAccount];
2304 // Now, where are we....
2305 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
2306 checkItem:[self checkPasswordBlock:self.keychainZoneID account:itemAccount password:@"newpassword"]];
2308 CKKSScanLocalItemsOperation* scanLocal = [self.keychainView scanLocalItems:@"test-scan"];
2309 [scanLocal waitUntilFinished];
2311 XCTAssertEqual(scanLocal.recordsAdded, 1u, "Should have added a single record");
2313 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2315 // And ensure that all four items are present again
2316 [self findGenericPassword: @"first" expecting: errSecSuccess];
2317 [self findGenericPassword: @"second" expecting: errSecSuccess];
2320 - (void)testResyncLocal {
2321 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2322 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2323 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2325 [self addGenericPassword: @"data" account: @"first"];
2326 [self addGenericPassword: @"data" account: @"second"];
2327 NSUInteger passwordCount = 2u;
2329 [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2330 [self startCKKSSubsystem];
2332 // Wait for uploads to happen
2333 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2334 [self waitForCKModifications];
2336 // Local resyncs shouldn't fetch clouds.
2337 self.silentFetchesAllowed = false;
2339 [self deleteGenericPassword:@"first"];
2340 [self deleteGenericPassword:@"second"];
2343 // And they're gone!
2344 [self findGenericPassword:@"first" expecting:errSecItemNotFound];
2345 [self findGenericPassword:@"second" expecting:errSecItemNotFound];
2347 CKKSLocalSynchronizeOperation* op = [self.keychainView resyncLocal];
2348 [op waitUntilFinished];
2349 XCTAssertNil(op.error, "Shouldn't be an error resyncing locally");
2351 // And they're back!
2352 [self checkGenericPassword: @"data" account: @"first"];
2353 [self checkGenericPassword: @"data" account: @"second"];
2356 - (void)testPlistRestoreResyncsLocal {
2357 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2358 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2359 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2361 [self addGenericPassword: @"data" account: @"first"];
2362 [self addGenericPassword: @"data" account: @"second"];
2363 NSUInteger passwordCount = 2u;
2365 [self checkGenericPassword: @"data" account: @"first"];
2366 [self checkGenericPassword: @"data" account: @"second"];
2368 [self expectCKModifyItemRecords:passwordCount currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2369 [self startCKKSSubsystem];
2371 // Wait for uploads to happen
2372 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2373 [self waitForCKModifications];
2376 // This 'restores' a plist keychain backup
2377 // That will kick off a local resync in CKKS, so hold that until we're ready...
2378 self.keychainView.holdLocalSynchronizeOperation = [CKKSResultOperation named:@"hold-local-synchronize" withBlock:^{}];
2380 // Local resyncs shouldn't fetch clouds.
2381 self.silentFetchesAllowed = false;
2383 CFErrorRef cferror = NULL;
2384 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
2385 CFErrorRef cfcferror = NULL;
2387 bool ret = SecServerImportKeychainInPlist(dbt, SecSecurityClientGet(), KEYBAG_NONE, KEYBAG_NONE,
2388 (__bridge CFDictionaryRef)@{}, kSecBackupableItemFilter, false, &cfcferror);
2390 XCTAssertNil(CFBridgingRelease(cfcferror), "Shouldn't error importing a 'backup'");
2391 XCTAssert(ret, "Importing a 'backup' should have succeeded");
2394 XCTAssertNil(CFBridgingRelease(cferror), "Shouldn't error mucking about in the db");
2396 // Restore is additive so original items stick around
2397 [self findGenericPassword:@"first" expecting:errSecSuccess];
2398 [self findGenericPassword:@"second" expecting:errSecSuccess];
2400 // Allow the local resync to continue...
2401 [self.operationQueue addOperation:self.keychainView.holdLocalSynchronizeOperation];
2402 [self.keychainView waitForOperationsOfClass:[CKKSLocalSynchronizeOperation class]];
2404 // Items are still here!
2405 [self checkGenericPassword: @"data" account: @"first"];
2406 [self checkGenericPassword: @"data" account: @"second"];
2409 - (void)testMultipleZoneAdd {
2410 // Bring up a new zone: we expect a key hierarchy upload.
2411 CKKSKeychainView* atvView = [self.injectedManager findOrCreateView:(id)kSecAttrViewHintAppleTV];
2412 [self.ckksViews addObject:atvView];
2413 CKRecordZoneID* appleTVZoneID = [[CKRecordZoneID alloc] initWithZoneName:(__bridge NSString*) kSecAttrViewHintAppleTV ownerName:CKCurrentUserDefaultName];
2415 // We also expect the view manager's notifyNewTLKsInKeychain call to fire once (after some delay)
2416 OCMExpect([self.mockCKKSViewManager notifyNewTLKsInKeychain]);
2418 // Let the horses loose
2419 [self startCKKSSubsystem];
2420 [self performOctagonTLKUpload:self.ckksViews];
2422 // We expect a single record to be uploaded to the 'keychain' view
2423 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2424 [self addGenericPassword: @"data" account: @"account-delete-me"];
2425 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2427 // We expect a single record to be uploaded to the 'atv' view
2428 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:appleTVZoneID];
2429 [self addGenericPassword: @"atv"
2430 account: @"tvaccount"
2431 viewHint:(__bridge NSString*) kSecAttrViewHintAppleTV
2432 access:(id)kSecAttrAccessibleAfterFirstUnlock
2433 expecting:errSecSuccess message:@"AppleTV view-hinted object"];
2435 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2437 OCMVerifyAllWithDelay(self.mockCKKSViewManager, 10);
2440 - (void)testMultipleZoneDelete {
2441 [self startCKKSSubsystem];
2443 // Bring up a new zone: we expect a key hierarchy and an item.
2444 CKKSKeychainView* atvView = [self.injectedManager findOrCreateView:(id)kSecAttrViewHintAppleTV];
2445 XCTAssertNotNil(atvView, "Should have a new ATV view");
2446 [self.ckksViews addObject:atvView];
2447 [self beginSOSTrustedViewOperation:atvView];
2448 CKRecordZoneID* appleTVZoneID = [[CKRecordZoneID alloc] initWithZoneName:(__bridge NSString*) kSecAttrViewHintAppleTV ownerName:CKCurrentUserDefaultName];
2450 [self performOctagonTLKUpload:self.ckksViews];
2452 // We expect a single record to be uploaded.
2453 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2454 [self addGenericPassword: @"data" account: @"account-delete-me"];
2455 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2457 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:appleTVZoneID];
2458 [self addGenericPassword: @"atv"
2459 account: @"tvaccount"
2460 viewHint:(__bridge NSString*) kSecAttrViewHintAppleTV
2461 access:(id)kSecAttrAccessibleAfterFirstUnlock
2462 expecting:errSecSuccess
2463 message:@"AppleTV view-hinted object"];
2464 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2466 // We expect a single record to be deleted from the ATV zone
2467 [self expectCKDeleteItemRecords: 1 zoneID:appleTVZoneID];
2468 [self deleteGenericPassword:@"tvaccount"];
2469 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2471 // Now we expect a single record to be deleted from the test zone
2472 [self expectCKDeleteItemRecords: 1 zoneID:self.keychainZoneID];
2473 [self deleteGenericPassword:@"account-delete-me"];
2474 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2477 - (void)testRestartWithoutRefetch {
2478 // Restarting the CKKS operation should check that it's been 15 minutes since the last fetch before it fetches again. Simulate this.
2479 [self startCKKSSubsystem];
2480 [self performOctagonTLKUpload:self.ckksViews];
2482 [self waitForCKModifications];
2483 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2485 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
2487 // Tear down the CKKS object and disallow fetches
2488 [self.keychainView halt];
2489 self.silentFetchesAllowed = false;
2491 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2492 [self beginSOSTrustedViewOperation:self.keychainView];
2493 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
2494 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2496 // Okay, cool, rad, now let's set the date to be very long ago and check that there's positively a fetch
2497 [self.keychainView halt];
2498 self.silentFetchesAllowed = false;
2500 [self.keychainView dispatchSync: ^bool {
2501 NSError* error = nil;
2502 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
2504 XCTAssertNil(error, "no error pulling ckse from database");
2505 XCTAssertNotNil(ckse, "received a ckse");
2507 ckse.lastFetchTime = [NSDate distantPast];
2508 [ckse saveToDatabase: &error];
2509 XCTAssertNil(error, "no error saving to database");
2513 [self expectCKFetch];
2514 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2515 [self beginSOSTrustedViewOperation:self.keychainView];
2516 [self.keychainView waitForKeyHierarchyReadiness];
2517 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2520 - (void)testRecoverFromZoneCreationFailure {
2521 // Fail the zone creation.
2522 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
2523 [self failNextZoneCreation:self.keychainZoneID];
2525 // Spin up CKKS subsystem.
2526 [self startCKKSSubsystem];
2528 // CKKS should figure it out, and fix it
2529 [self performOctagonTLKUpload:self.ckksViews];
2530 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2532 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2533 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2534 [self addGenericPassword: @"data" account: @"account-delete-me"];
2535 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2537 XCTAssertNil(self.zones[self.keychainZoneID].creationError, "Creation error was unset (and so CKKS probably dealt with the error");
2540 - (void)testRecoverFromZoneSubscriptionFailure {
2541 // Fail the zone subscription.
2542 [self failNextZoneSubscription:self.keychainZoneID];
2544 // Spin up CKKS subsystem.
2545 [self startCKKSSubsystem];
2547 // The CKKS subsystem should figure out the issue, and fix it before Octagon uploads its items
2548 [self performOctagonTLKUpload:self.ckksViews];
2550 [self.keychainView waitForKeyHierarchyReadiness];
2551 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2553 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2554 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2555 [self addGenericPassword: @"data" account: @"account-delete-me"];
2556 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2558 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
2561 - (void)testRecoverFromZoneSubscriptionFailureDueToZoneNotExisting {
2562 // This is different from testRecoverFromZoneSubscriptionFailure, since the zone is gone. CKKS must attempt to re-create the zone.
2564 // Silently fail the zone creation
2565 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
2566 [self failNextZoneCreationSilently:self.keychainZoneID];
2568 // Spin up CKKS subsystem.
2569 [self startCKKSSubsystem];
2571 // The CKKS subsystem should figure out the issue, and fix it.
2572 [self performOctagonTLKUpload:self.ckksViews];
2574 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
2575 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2577 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2578 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2579 [self addGenericPassword: @"data" account: @"account-delete-me"];
2580 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2582 XCTAssertFalse(self.zones[self.keychainZoneID].flag, "Zone flag was reset");
2583 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
2586 - (void)testRecoverFromDeletedTLKWithStashedTLK {
2587 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
2589 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2590 NSError* error = nil;
2593 [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
2594 XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
2596 // And delete the non-stashed version
2597 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
2598 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
2600 // Spin up CKKS subsystem.
2601 [self startCKKSSubsystem];
2603 [self.keychainView waitForKeyHierarchyReadiness];
2604 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2606 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2607 [self addGenericPassword: @"data" account: @"account-delete-me"];
2608 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2610 // CKKS should recreate the syncable TLK.
2611 [self checkNSyncableTLKsInKeychain: 1];
2614 - (void)testRecoverFromDeletedTLKWithStashedTLKUponRestart {
2615 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
2617 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2618 // Spin up CKKS subsystem.
2619 [self startCKKSSubsystem];
2620 [self.keychainView waitForKeyHierarchyReadiness];
2622 // Tear down the CKKS object
2623 [self.keychainView halt];
2625 NSError* error = nil;
2628 [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
2629 XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
2631 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
2632 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
2634 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2635 [self beginSOSTrustedViewOperation:self.keychainView];
2636 [self.keychainView waitForKeyHierarchyReadiness];
2637 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2639 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2640 [self addGenericPassword: @"data" account: @"account-delete-me"];
2641 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2643 // CKKS should recreate the syncable TLK.
2644 [self checkNSyncableTLKsInKeychain: 1];
2648 // <rdar://problem/49024967> Octagon: tests for CK exceptions out of cuttlefish
2649 - (void)testRecoverFromTLKWriteFailure {
2650 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
2651 // Test starts with nothing in CloudKit, and will fail the first TLK write.
2652 NSError* noNetwork = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}];
2653 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:noNetwork];
2655 // Spin up CKKS subsystem.
2656 [self startCKKSSubsystem];
2658 // The CKKS subsystem should figure out the issue, and fix it.
2659 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2661 [self.keychainView waitForKeyHierarchyReadiness];
2662 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2664 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2665 [self addGenericPassword: @"data" account: @"account-delete-me"];
2666 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2668 // A network failure creating new TLKs shouldn't delete the 'failed' syncable one.
2669 [self checkNSyncableTLKsInKeychain: 2];
2673 // This test needs to be moved and rewritten now that Octagon handles TLK uploads
2674 // <rdar://problem/49024967> Octagon: tests for CK exceptions out of cuttlefish
2676 - (void)testRecoverFromTLKRace {
2677 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
2678 // Test starts with nothing in CloudKit, and will fail the first TLK write.
2679 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject: ^{
2680 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2683 // Spin up CKKS subsystem.
2684 [self startCKKSSubsystem];
2686 // The first TLK write should fail, and then our fake TLKs should be there in CloudKit.
2687 // It shouldn't write anything back up to CloudKit.
2688 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2690 // Now the TLKs arrive from the other device...
2691 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2692 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2693 [self.keychainView waitForKeyHierarchyReadiness];
2695 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2696 [self addGenericPassword: @"data" account: @"account-delete-me"];
2697 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2699 // A race failure creating new TLKs should delete the old syncable one.
2700 [self checkNSyncableTLKsInKeychain: 1];
2704 - (void)testRecoverFromNullCurrentKeyPointers {
2705 // The current key pointers in cloudkit shouldn't ever not exist if keys do. But, if they don't, CKKS must recover.
2707 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
2708 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2709 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2711 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
2712 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
2713 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2714 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2715 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2717 // Spin up CKKS subsystem.
2718 [self startCKKSSubsystem];
2720 // The CKKS subsystem should figure out the issue, and fix it.
2721 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
2723 [self.keychainView waitForKeyHierarchyReadiness];
2725 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2728 - (void)testRecoverFromNoCurrentKeyPointers {
2729 // The current key pointers in cloudkit shouldn't ever point to nil. But, if they do, CKKS must recover.
2731 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
2732 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2733 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2735 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
2736 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentTLKPointer.storedCKRecord.recordID], "Deleted TLK pointer from zone");
2737 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassAPointer.storedCKRecord.recordID], "Deleted class a pointer from zone");
2738 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassCPointer.storedCKRecord.recordID], "Deleted class c pointer from zone");
2740 // Spin up CKKS subsystem.
2741 [self startCKKSSubsystem];
2743 // The CKKS subsystem should figure out the issue, and fix it.
2744 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
2746 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
2748 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2752 // <rdar://problem/49024967> Octagon: tests for CK exceptions out of cuttlefish
2753 - (void)testRecoverFromBadChangeTag {
2754 // 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.
2756 // Test starts with a broken key hierarchy in our fake CloudKit, but a (incorrectly) up-to-date change tag stored locally.
2757 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2758 SecCKKSTestSetDisableKeyNotifications(true); // Don't tell CKKS about this key material; we're pretending like this is a securityd restart
2759 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2760 SecCKKSTestSetDisableKeyNotifications(false);
2762 [self.keychainView dispatchSync: ^bool {
2763 NSError* error = nil;
2764 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainZoneID.zoneName];
2765 XCTAssertNotNil(ckse, "should have received a ckse");
2767 ckse.ckzonecreated = true;
2768 ckse.ckzonesubscribed = true;
2769 ckse.changeToken = self.keychainZone.currentChangeToken;
2771 [ckse saveToDatabase: &error];
2772 XCTAssertNil(error, "shouldn't have gotten an error saving to database");
2776 // The CKKS subsystem should try to write TLKs, but fail. It'll then upload a TLK share for the keys already in CloudKit
2777 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
2778 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2780 // Spin up CKKS subsystem.
2781 [self startCKKSSubsystem];
2782 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2784 // CKKS should then happily use the keys in CloudKit
2785 [self createClassCItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me"];
2786 [self createClassAItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me-class-a"];
2790 - (void)testRecoverFromDeletedKeysNewItem {
2791 [self startCKKSSubsystem];
2792 [self performOctagonTLKUpload:self.ckksViews];
2794 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
2796 // We expect a single class C record to be uploaded.
2797 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2798 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2800 [self addGenericPassword: @"data" account: @"account-delete-me"];
2801 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2803 [self waitForCKModifications];
2804 [self.keychainView waitUntilAllOperationsAreFinished];
2806 // Now, delete the local keys from the keychain (but leave the synced TLK)
2807 SecCKKSTestSetDisableKeyNotifications(true);
2808 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2809 (id)kSecClass : (id)kSecClassInternetPassword,
2810 (id)kSecUseDataProtectionKeychain : @YES,
2811 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2812 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
2813 }), @"Deleting local keys");
2814 SecCKKSTestSetDisableKeyNotifications(false);
2816 NSError* error = nil;
2817 [self.keychainZoneKeys.classC loadKeyMaterialFromKeychain:&error];
2818 XCTAssertNotNil(error, "Error loading class C key material from keychain");
2820 // We expect a single class C record to be uploaded.
2821 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2822 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2824 [self addGenericPassword: @"datadata" account: @"account-no-keys"];
2825 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2827 // We expect a single class A record to be uploaded.
2828 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2829 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2830 [self addGenericPassword:@"asdf"
2831 account:@"account-class-A"
2833 access:(id)kSecAttrAccessibleWhenUnlocked
2834 expecting:errSecSuccess
2835 message:@"Adding class A item"];
2836 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2839 - (void)testRecoverFromDeletedKeysReceive {
2840 [self startCKKSSubsystem];
2841 [self performOctagonTLKUpload:self.ckksViews];
2843 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
2845 [self waitForCKModifications];
2846 [self.keychainView waitUntilAllOperationsAreFinished];
2848 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2850 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
2852 // Now, delete the local keys from the keychain (but leave the synced TLK)
2853 SecCKKSTestSetDisableKeyNotifications(true);
2854 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2855 (id)kSecClass : (id)kSecClassInternetPassword,
2856 (id)kSecUseDataProtectionKeychain : @YES,
2857 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2858 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
2859 }), @"Deleting local keys");
2860 SecCKKSTestSetDisableKeyNotifications(false);
2862 // Trigger a notification (with hilariously fake data)
2863 [self.keychainZone addToZone: ckr];
2864 [self.keychainView notifyZoneChange:nil];
2865 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2866 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2868 [self findGenericPassword: @"account0" expecting:errSecSuccess];
2871 - (void)testRecoverDeletedTLK {
2872 // If the TLK disappears halfway through, well, that's no good. But we should recover using TLK sharing
2874 [self startCKKSSubsystem];
2875 [self performOctagonTLKUpload:self.ckksViews];
2877 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
2879 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2880 [self waitForCKModifications];
2882 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
2883 [self.keychainView waitUntilAllOperationsAreFinished];
2885 // Now, delete the local keys from the keychain
2886 SecCKKSTestSetDisableKeyNotifications(true);
2887 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2888 (id)kSecClass : (id)kSecClassInternetPassword,
2889 (id)kSecUseDataProtectionKeychain : @YES,
2890 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2891 (id)kSecAttrSynchronizable : (id)kSecAttrSynchronizableAny,
2892 }), @"Deleting CKKS keys");
2893 SecCKKSTestSetDisableKeyNotifications(false);
2895 // Trigger a notification (with hilariously fake data)
2896 [self.keychainZone addToZone: ckr];
2897 [self.keychainView notifyZoneChange:nil];
2899 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2901 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should return to 'ready'");
2903 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // Do this again, to allow for non-atomic key state machinery switching
2905 [self findGenericPassword: @"account0" expecting:errSecSuccess];
2908 - (void)testRecoverMissingRolledKey {
2909 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2911 NSString* accountShouldExist = @"under-rolled-key";
2912 NSString* accountWillExist = @"under-rolled-key-later";
2913 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountShouldExist];
2914 [self.keychainZone addToZone: ckr];
2916 CKRecord* ckrAddedLater = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountWillExist];
2917 CKKSKey* pastClassCKey = self.keychainZoneKeys.classC;
2919 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2920 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2922 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2924 [self startCKKSSubsystem];
2925 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
2927 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2928 [self waitForCKModifications];
2930 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2931 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2932 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
2934 // Now, find and delete the class C key that ckrAddedLater is under
2935 NSError* error = nil;
2936 XCTAssertTrue([pastClassCKey deleteKeyMaterialFromKeychain:&error], "Should be able to delete old key material from keychain");
2937 XCTAssertNil(error, "Should be no error deleting old key material from keychain");
2939 [self.keychainZone addToZone:ckrAddedLater];
2940 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2942 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2943 [self findGenericPassword:accountWillExist expecting:errSecSuccess];
2945 XCTAssertTrue([pastClassCKey loadKeyMaterialFromKeychain:&error], "Class C key should be back in the keychain");
2946 XCTAssertNil(error, "Should be no error loading key from keychain");
2949 - (void)testRecoverMissingRolledClassAKeyWhileLocked {
2950 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2952 NSString* accountShouldExist = @"under-rolled-key";
2953 NSString* accountWillExist = @"under-rolled-key-later";
2954 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountShouldExist key:self.keychainZoneKeys.classA];
2955 [self.keychainZone addToZone: ckr];
2957 CKRecord* ckrAddedLater = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountWillExist key:self.keychainZoneKeys.classA];
2958 CKKSKey* pastClassAKey = self.keychainZoneKeys.classA;
2960 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2961 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2963 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2965 [self startCKKSSubsystem];
2966 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
2968 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2969 [self waitForCKModifications];
2971 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2972 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2973 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
2975 // Now, find and delete the class C key that ckrAddedLater is under
2976 NSError* error = nil;
2977 XCTAssertTrue([pastClassAKey deleteKeyMaterialFromKeychain:&error], "Should be able to delete old key material from keychain");
2978 XCTAssertNil(error, "Should be no error deleting old key material from keychain");
2980 // now, lock the keychain
2981 self.aksLockState = true;
2982 [self.lockStateTracker recheck];
2984 [self.keychainZone addToZone:ckrAddedLater];
2985 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2987 // Item should still not exist due to the lock state....
2988 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2989 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
2991 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], "Key state should have returned to readypendingunlock");
2993 self.aksLockState = false;
2994 [self.lockStateTracker recheck];
2997 [self.keychainView waitUntilAllOperationsAreFinished];
2998 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2999 [self findGenericPassword:accountWillExist expecting:errSecSuccess];
3001 XCTAssertTrue([pastClassAKey loadKeyMaterialFromKeychain:&error], "Class A key should be back in the keychain");
3002 XCTAssertNil(error, "Should be no error loading key from keychain");
3005 - (void)testRecoverFromBadCurrentKeyPointer {
3006 // The current key pointers in cloudkit shouldn't ever point to missing entries. But, if they do, CKKS must recover.
3008 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
3009 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3010 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3012 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
3013 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
3014 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real tlk" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
3015 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class a key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
3016 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class c key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
3018 // Spin up CKKS subsystem.
3019 [self startCKKSSubsystem];
3021 // The CKKS subsystem should figure out the issue, and fix it (while uploading itself a TLK Share)
3022 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3024 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
3026 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3029 - (void)testRecoverFromIncorrectCurrentTLKPointer {
3030 // The current key pointers in cloudkit shouldn't ever point to wrong entries. But, if they do, CKKS must recover.
3032 // Test starts with a rolled hierarchy, and CKPs pointing to the wrong items
3033 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3034 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3036 CKKSCurrentKeyPointer* oldTLKCKP = self.keychainZoneKeys.currentTLKPointer;
3037 CKRecord* oldTLKPointer = [self.keychainZone.currentDatabase[self.keychainZoneKeys.currentTLKPointer.storedCKRecord.recordID] copy];
3039 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3040 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3042 ZoneKeys* newZoneKeys = [self.keychainZoneKeys copy];
3044 // And put the oldTLKPointer back
3045 [self.zones[self.keychainZoneID] addToZone:oldTLKPointer];
3046 self.keychainZoneKeys.currentTLKPointer = oldTLKCKP;
3048 // Make sure it stuck:
3049 XCTAssertNotEqualObjects(self.keychainZoneKeys.currentTLKPointer,
3050 newZoneKeys.currentTLKPointer,
3051 "current TLK pointer should now not point to proper TLK");
3053 // Spin up CKKS subsystem.
3054 [self startCKKSSubsystem];
3056 // The CKKS subsystem should figure out the issue, and fix it (while uploading itself a TLK Share)
3057 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
3059 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
3061 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3062 [self waitForCKModifications];
3064 XCTAssertEqualObjects(self.keychainZoneKeys.currentTLKPointer,
3065 newZoneKeys.currentTLKPointer,
3066 "current TLK pointer should now point to proper TLK");
3067 XCTAssertEqualObjects(self.keychainZoneKeys.currentClassAPointer,
3068 newZoneKeys.currentClassAPointer,
3069 "current Class A pointer should now point to proper Class A key");
3070 XCTAssertEqualObjects(self.keychainZoneKeys.currentClassCPointer,
3071 newZoneKeys.currentClassCPointer,
3072 "current Class C pointer should now point to proper Class C key");
3075 - (void)testRecoverFromDesyncedKeyRecordsViaResync {
3076 // We need to set up a desynced situation to test our resync.
3077 // First, let CKKS start up and send several items to CloudKit (that we'll then desync!)
3078 __block NSError* error = nil;
3080 // Test starts with keys in CloudKit (so we can create items later)
3081 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3082 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3083 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3085 [self addGenericPassword: @"data" account: @"first"];
3086 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
3088 [self startCKKSSubsystem];
3090 // Wait for uploads to happen
3091 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3092 [self waitForCKModifications];
3094 // Now, delete most of the key records are from on-disk, but the change token is not changed
3095 [self.keychainView dispatchSync:^bool{
3096 CKKSCurrentKeySet* keyset = [CKKSCurrentKeySet loadForZone:self.keychainZoneID];
3098 XCTAssertNotNil(keyset.currentTLKPointer, @"should be a TLK pointer");
3099 XCTAssertNotNil(keyset.currentClassAPointer, @"should be a class A pointer");
3100 XCTAssertNotNil(keyset.currentClassCPointer, @"should be a class C pointer");
3102 [keyset.currentTLKPointer deleteFromDatabase:&error];
3103 XCTAssertNil(error, "Should be no error deleting TLK pointer from database");
3104 [keyset.currentClassAPointer deleteFromDatabase:&error];
3105 XCTAssertNil(error, "Should be no error deleting class A pointer from database");
3107 XCTAssertNotNil(keyset.tlk, @"should be a TLK");
3108 XCTAssertNotNil(keyset.classA, @"should be a classA key");
3109 XCTAssertNotNil(keyset.classC, @"should be a classC key");
3111 [keyset.tlk deleteFromDatabase:&error];
3112 XCTAssertNil(error, "Should be no error deleting TLK from database");
3114 [keyset.classA deleteFromDatabase:&error];
3115 XCTAssertNil(error, "Should be no error deleting classA from database");
3117 [keyset.classC deleteFromDatabase:&error];
3118 XCTAssertNil(error, "Should be no error deleting classC from database");
3123 // A restart should realize there's an issue, and pause for help
3124 // Ideally we'd try a refetch here to see if we're very wrong, but that's hard to avoid making into an infinite loop
3125 [self.keychainView halt];
3126 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
3127 [self.keychainView beginCloudKitOperation];
3128 [self beginSOSTrustedViewOperation:self.keychainView];
3130 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"key state should enter 'waitfortlkcreation'");
3132 // But, a resync should fix you back up
3133 CKKSSynchronizeOperation* resyncOperation = [self.keychainView resyncWithCloud];
3134 [resyncOperation waitUntilFinished];
3135 XCTAssertNil(resyncOperation.error, "No error during the resync operation");
3137 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
3140 - (void)testRecoverFromCloudKitFetchFail {
3141 // Test starts with nothing in database, but one in our fake CloudKit.
3142 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3143 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
3145 // The first two CKRecordZoneChanges should fail with a 'network unavailable' error.
3146 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
3147 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
3149 // Spin up CKKS subsystem.
3150 [self startCKKSSubsystem];
3152 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3153 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3154 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3156 // We expect a single record to be uploaded
3157 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3158 [self addGenericPassword: @"data" account: @"account-delete-me"];
3159 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3161 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3162 [self addGenericPassword:@"asdf"
3163 account:@"account-class-A"
3165 access:(id)kSecAttrAccessibleWhenUnlocked
3166 expecting:errSecSuccess
3167 message:@"Adding class A item"];
3168 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3171 - (void)testRecoverFromCloudKitFetchNetworkFailAfterReady {
3172 // Test starts with nothing in database, but one in our fake CloudKit.
3173 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3175 // Spin up CKKS subsystem.
3176 [self startCKKSSubsystem];
3178 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
3179 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateReady, "CKKS entered ready");
3181 // Network is unavailable
3182 [self.reachabilityTracker setNetworkReachability:false];
3184 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
3185 [self.keychainZone addToZone:ckr];
3187 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3189 // Say network is available
3190 [self.reachabilityTracker setNetworkReachability:true];
3192 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3194 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3197 - (void)testRecoverFromCloudKitFetchNetworkFailBeforeReady {
3198 // Test starts with nothing in database, but one in our fake CloudKit.
3199 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3201 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
3202 [self.keychainZone addToZone:ckr];
3204 // Network is unavailable
3205 [self.reachabilityTracker setNetworkReachability:false];
3207 // Spin up CKKS subsystem.
3208 [self startCKKSSubsystem];
3210 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateInitializing] wait:20*NSEC_PER_SEC], "CKKS entered initializing");
3211 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateInitializing, "CKKS entered initializing");
3213 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3214 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3215 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3217 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3219 // Say network is available
3220 [self.reachabilityTracker setNetworkReachability:true];
3222 [self.keychainView waitUntilAllOperationsAreFinished];
3223 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3225 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3228 - (void)testWaitAfterCloudKitNetworkFailDuringOutgoingQueueOperation {
3229 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3231 [self startCKKSSubsystem];
3233 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered ready");
3235 // Network is now unavailable
3236 [self.reachabilityTracker setNetworkReachability:false];
3238 NSError* noNetwork = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{
3239 CKErrorRetryAfterKey: @(0.2),
3241 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:noNetwork];
3242 [self addGenericPassword: @"data" account: @"account-delete-me"];
3244 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3247 // Once network is available again, the write should happen
3248 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
3249 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3251 [self.reachabilityTracker setNetworkReachability:true];
3253 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3255 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3258 - (void)testRecoverFromCloudKitFetchFailWithDelay {
3259 // Test starts with nothing in database, but one in our fake CloudKit.
3260 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3261 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
3263 // The first CKRecordZoneChanges should fail with a 'delay' error.
3264 self.silentFetchesAllowed = false;
3265 [self.keychainZone failNextFetchWith:[[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorRequestRateLimited userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:4]}]];
3266 [self expectCKFetch];
3268 // Spin up CKKS subsystem.
3269 [self startCKKSSubsystem];
3271 // Ensure it doesn't fetch within these three seconds (if it does, an exception will throw).
3274 // Okay, you can fetch again.
3275 [self expectCKFetch];
3277 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3278 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3279 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3281 // We expect a single record to be uploaded
3282 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3283 [self addGenericPassword: @"data" account: @"account-delete-me"];
3284 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3287 - (void)testHandleZoneDeletedWhileFetching {
3288 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
3289 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3290 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3291 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3293 // The first CKRecordZoneChanges should fail with a 'zone not found' error (race between zone creation as part of initalization and zone deletion from another device)
3294 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorZoneNotFound userInfo:@{}]];
3296 [self startCKKSSubsystem];
3298 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"Key state should become 'ready'");
3301 - (void)testRecoverFromCloudKitOldChangeToken {
3302 // Test starts with nothing in database, but one in our fake CloudKit.
3303 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3305 // Spin up CKKS subsystem.
3306 [self startCKKSSubsystem];
3308 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3309 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3310 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3312 // We expect a single record to be uploaded
3313 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3314 [self addGenericPassword: @"data" account: @"account-delete-me"];
3315 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3317 // Delete all old database states, to destroy the change tag validity
3318 [self.keychainZone.pastDatabases removeAllObjects];
3320 // We expect a total local flush and refetch
3321 self.silentFetchesAllowed = false;
3322 [self expectCKFetch]; // one to fail with a CKErrorChangeTokenExpired error
3323 [self expectCKFetch]; // and one to succeed
3325 // Trigger a fake change notification
3326 [self.keychainView notifyZoneChange:nil];
3328 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3330 // And check that a new upload happens just fine.
3331 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3332 [self addGenericPassword:@"asdf"
3333 account:@"account-class-A"
3335 access:(id)kSecAttrAccessibleWhenUnlocked
3336 expecting:errSecSuccess
3337 message:@"Adding class A item"];
3338 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3341 - (void)testRecoverFromCloudKitUnknownDeviceStateRecord {
3342 // Test starts with nothing in database, but one in our fake CloudKit.
3343 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3344 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3345 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3347 // Save a new device state record with some fake etag
3348 [self.keychainView dispatchSync: ^bool {
3349 CKKSDeviceStateEntry* cdse = [[CKKSDeviceStateEntry alloc] initForDevice:self.ckDeviceID
3350 osVersion:@"fake-record"
3351 lastUnlockTime:[NSDate date]
3354 circlePeerID:self.mockSOSAdapter.selfPeer.peerID
3355 circleStatus:kSOSCCInCircle
3356 keyState:SecCKKSZoneKeyStateWaitForTLK
3358 currentClassAUUID:nil
3359 currentClassCUUID:nil
3360 zoneID:self.keychainZoneID
3361 encodedCKRecord:nil];
3362 XCTAssertNotNil(cdse, "Should have created a fake CDSE");
3363 CKRecord* record = [cdse CKRecordWithZoneID:self.keychainZoneID];
3364 XCTAssertNotNil(record, "Should have created a fake CDSE CKRecord");
3365 record.etag = @"fake etag";
3366 cdse.storedCKRecord = record;
3368 NSError* error = nil;
3369 [cdse saveToDatabase:&error];
3370 XCTAssertNil(error, @"No error saving cdse to database");
3375 // Spin up CKKS subsystem.
3376 [self startCKKSSubsystem];
3378 // We expect a record failure, since the device state record is broke
3379 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
3381 // And then we expect a clean write
3382 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
3383 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3385 [self addGenericPassword: @"data" account: @"account-delete-me"];
3386 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3389 - (void)testRecoverFromCloudKitUnknownItemRecord {
3390 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3392 // Spin up CKKS subsystem.
3393 [self startCKKSSubsystem];
3395 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3397 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
3398 [self.keychainZone addToZone:ckr];
3400 [self.keychainView notifyZoneChange:nil];
3401 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3403 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3405 // Delete the record from CloudKit, but miss the notification
3406 XCTAssertNil([self.keychainZone deleteCKRecordIDFromZone: ckr.recordID], "Deleting the record from fake CloudKit should succeed");
3408 // Expect a failed upload when we modify the item
3409 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
3410 [self updateGenericPassword:@"never seen again" account:@"account-delete-me"];
3411 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3413 [self.keychainView waitUntilAllOperationsAreFinished];
3415 // And the item should be disappeared from the local keychain
3416 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3419 - (void)testRecoverFromCloudKitUserDeletedZone {
3420 // Test starts with nothing in database, but one in our fake CloudKit.
3421 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3423 // Spin up CKKS subsystem.
3424 [self startCKKSSubsystem];
3426 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3427 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3428 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3430 // We expect a single record to be uploaded
3431 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3432 [self addGenericPassword: @"data" account: @"account-delete-me"];
3433 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3435 // The first CKRecordZoneChanges should fail with a 'CKErrorUserDeletedZone' error. This will cause a local reset, ending up with zone re-creation.
3436 self.zones[self.keychainZoneID] = nil; // delete the zone
3437 self.keys[self.keychainZoneID] = nil;
3438 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorUserDeletedZone userInfo:@{}]];
3440 // We expect CKKS to recreate the zone, then have octagon reupload the keys, and then the class C item upload
3441 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3443 [self.keychainView notifyZoneChange:nil];
3445 [self performOctagonTLKUpload:self.ckksViews];
3447 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3449 // And check that a new upload occurs.
3450 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3452 [self addGenericPassword:@"asdf"
3453 account:@"account-class-A"
3455 access:(id)kSecAttrAccessibleWhenUnlocked
3456 expecting:errSecSuccess
3457 message:@"Adding class A item"];
3458 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3461 - (void)testRecoverFromCloudKitZoneNotFoundWithoutZoneDeletion {
3462 // Test starts with nothing in database, but one in our fake CloudKit.
3463 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3464 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
3466 // Spin up CKKS subsystem.
3467 [self startCKKSSubsystem];
3469 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3470 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3471 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3473 // We expect a single record to be uploaded
3474 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3475 [self addGenericPassword: @"data" account: @"account-delete-me"];
3476 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3478 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
3480 [self waitForCKModifications];
3481 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
3483 // The next CKRecordZoneChanges will fail with a 'zone not found' error.
3484 self.zones[self.keychainZoneID] = nil; // delete the zone
3485 self.keys[self.keychainZoneID] = nil;
3487 // We expect CKKS to reset itself and recover, then have octagon upload the keys, and then the class C item upload
3488 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3490 [self.keychainView notifyZoneChange:nil];
3492 [self performOctagonTLKUpload:self.ckksViews];
3493 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3494 [self waitForCKModifications];
3496 // And check that a new upload occurs.
3497 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3499 [self addGenericPassword:@"asdf"
3500 account:@"account-class-A"
3502 access:(id)kSecAttrAccessibleWhenUnlocked
3503 expecting:errSecSuccess
3504 message:@"Adding class A item"];
3505 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3508 - (void)testRecoverFromCloudKitZoneNotFoundFetchBeforeSigninOccurs {
3509 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
3511 // Before CKKS sign-in, it receives a fetch rpc
3512 XCTestExpectation *fetchReturns = [self expectationWithDescription:@"fetch returned"];
3513 [self.injectedManager rpcFetchAndProcessChanges:nil reply:^(NSError *result) {
3514 XCTAssertNil(result, "Should be no error fetching and processing changes");
3515 [fetchReturns fulfill];
3518 [self startCKKSSubsystem];
3520 [self performOctagonTLKUpload:self.ckksViews];
3521 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
3523 // We expect a single record to be uploaded
3524 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
3525 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3526 [self addGenericPassword: @"data" account: @"account-delete-me"];
3527 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3529 // The fetch should have come back by now
3530 [self waitForExpectations: @[fetchReturns] timeout:5];
3533 - (void)testNoCloudKitAccount {
3534 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
3535 self.accountStatus = CKAccountStatusNoAccount;
3536 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3538 self.silentFetchesAllowed = false;
3539 [self startCKKSSubsystem];
3541 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3543 [self addGenericPassword: @"data" account: @"account-delete-me"];
3544 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3546 // simulate a NSNotification callback (but still logged out)
3547 self.accountStatus = CKAccountStatusNoAccount;
3548 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3550 // There should be no further uploads, even when we save keychain items
3551 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3552 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3554 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3555 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3557 // Test that there are no items in the database (since we never logged in)
3558 [self checkNoCKKSData: self.keychainView];
3561 - (void)testSACloudKitAccount {
3562 // Test starts with nothing in database and the user logged into CloudKit and in circle, but the account is not HSA2.
3563 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3565 self.accountStatus = CKAccountStatusAvailable;
3567 self.silentFetchesAllowed = false;
3569 // Octagon does not initialize the ckks views when not in an HSA2 account
3570 self.automaticallyBeginCKKSViewCloudKitOperation = false;
3571 [self startCKKSSubsystem];
3573 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3575 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3576 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS should enter 'loggedout'");
3578 // There should be no uploads, even when we save keychain items and enter/exit circle
3579 [self addGenericPassword: @"data" account: @"account-delete-me"];
3580 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3582 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3583 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3584 [self endSOSTrustedOperationForAllViews];
3585 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3587 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3588 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3589 [self beginSOSTrustedViewOperation:self.keychainView];
3590 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3592 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3593 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3595 // Test that there are no items in the database (since we never were in an HSA2 account)
3596 [self checkNoCKKSData: self.keychainView];
3599 - (void)testEarlyLogin
3601 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3603 // Octagon should initialize these views
3604 self.automaticallyBeginCKKSViewCloudKitOperation = true;
3606 self.accountStatus = CKAccountStatusAvailable;
3607 //[self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3609 [self startCKKSSubsystem];
3611 // CKKS should end up in 'waitfortlkcreation', as there's no trust and no TLKs
3612 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortlkcreation'");
3614 // Now, renotify the account status, and ensure that CKKS doesn't reenter 'initializing'
3615 CKKSCondition* initializing = self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateInitializing];
3617 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3619 XCTAssertNotEqual(0, [initializing wait:500*NSEC_PER_MSEC], "CKKS should not enter initializing when the device HSA status changes");
3622 - (void)testNoCircle {
3623 // Test starts with nothing in database and the user logged into CloudKit, but out of Circle.
3624 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3626 self.accountStatus = CKAccountStatusAvailable;
3628 [self startCKKSSubsystem];
3630 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3632 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3634 [self addGenericPassword: @"data" account: @"account-delete-me"];
3635 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3637 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortlkcreation'");
3639 // simulate a NSNotification callback (but still logged out)
3640 self.accountStatus = CKAccountStatusNoAccount;
3641 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3643 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'loggedout'");
3645 // There should be no further uploads, even when we save keychain items
3646 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3647 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3649 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3650 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3652 // Test that there are no items in the database (since we never logged in)
3653 [self checkNoCKKSData: self.keychainView];
3656 - (void)testCircleDepartAndRejoin {
3657 // Test starts with CKKS in ready
3658 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3659 [self startCKKSSubsystem];
3661 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
3663 // But then, trust departs
3664 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3665 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3666 [self endSOSTrustedOperationForAllViews];
3668 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortrust'");
3670 // There should be no further uploads, even when we save keychain items
3671 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3672 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3674 // Then trust returns. We expect two uploads
3675 [self expectCKModifyItemRecords:2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
3676 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3677 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3678 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3679 [self beginSOSTrustedViewOperation:self.keychainView];
3681 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3682 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3685 - (void)testCloudKitLogin {
3686 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
3687 self.accountStatus = CKAccountStatusNoAccount;
3688 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3690 // Before we inform CKKS of its account state....
3691 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
3693 [self startCKKSSubsystem];
3695 XCTAssertEqual(0, [self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], "Should have been told of a 'logout' event on startup");
3696 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event shouldn't have happened");
3697 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3699 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3701 // simulate a cloudkit login and NSNotification callback
3702 self.accountStatus = CKAccountStatusAvailable;
3703 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3705 // No writes yet, since we're not in circle
3706 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortlkcreation'");
3707 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3709 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3710 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3711 [self beginSOSTrustedOperationForAllViews];
3713 [self performOctagonTLKUpload:self.ckksViews];
3715 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3716 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3717 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3719 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3720 [self waitForCKModifications];
3722 // We expect a single class C record to be uploaded.
3723 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3724 [self addGenericPassword: @"data" account: @"account-delete-me"];
3726 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3727 [self waitForCKModifications];
3730 - (void)testCloudKitLogoutLogin {
3731 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
3732 [self startCKKSSubsystem];
3733 [self performOctagonTLKUpload:self.ckksViews];
3734 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3735 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3736 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3738 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3739 [self waitForCKModifications];
3741 // We expect a single class C record to be uploaded.
3742 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3743 [self addGenericPassword: @"data" account: @"account-delete-me"];
3745 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3746 [self waitForCKModifications];
3748 // simulate a cloudkit logout and NSNotification callback
3749 self.accountStatus = CKAccountStatusNoAccount;
3750 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3751 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3752 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3753 [self endSOSTrustedOperationForAllViews];
3755 // Test that there are no items in the database after logout
3756 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
3757 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
3758 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3759 [self checkNoCKKSData: self.keychainView];
3761 // There should be no further uploads, even when we save keychain items
3762 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3763 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3765 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3766 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3767 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
3769 // simulate a cloudkit login
3770 // 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
3771 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
3773 self.accountStatus = CKAccountStatusAvailable;
3774 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3776 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3777 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3778 [self beginSOSTrustedViewOperation:self.keychainView];
3780 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3781 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3782 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3784 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3786 // Let everything settle...
3787 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3788 [self waitForCKModifications];
3791 self.accountStatus = CKAccountStatusNoAccount;
3792 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3794 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3795 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3796 [self endSOSTrustedOperationForAllViews];
3798 // Test that there are no items in the database after logout
3799 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
3800 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
3801 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3802 [self checkNoCKKSData: self.keychainView];
3804 // There should be no further uploads, even when we save keychain items
3805 [self addGenericPassword: @"data" account: @"account-delete-me-5"];
3806 [self addGenericPassword: @"data" account: @"account-delete-me-6"];
3808 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3809 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3811 // simulate a cloudkit login
3812 // 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
3813 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
3815 self.accountStatus = CKAccountStatusAvailable;
3816 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3818 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3819 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3820 [self beginSOSTrustedViewOperation:self.keychainView];
3822 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3823 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3824 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3826 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3828 // Let everything settle...
3829 [self.keychainView waitUntilAllOperationsAreFinished];
3830 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3833 self.accountStatus = CKAccountStatusNoAccount;
3834 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3836 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3837 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3838 [self endSOSTrustedOperationForAllViews];
3840 // Test that there are no items in the database after logout
3841 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
3842 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
3843 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3844 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
3845 [self checkNoCKKSData: self.keychainView];
3847 // Force zone into error state
3848 self.keychainView.keyHierarchyState = SecCKKSZoneKeyStateError;
3850 self.accountStatus = CKAccountStatusAvailable;
3851 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3853 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3854 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3855 [self beginSOSTrustedViewOperation:self.keychainView];
3857 XCTestExpectation *operationRun = [self expectationWithDescription:@"operation run"];
3858 NSOperation* op = [NSBlockOperation named:@"test" withBlock:^{
3859 [operationRun fulfill];
3862 [op addDependency:self.keychainView.keyStateReadyDependency];
3863 [self.operationQueue addOperation:op];
3865 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3866 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3867 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3869 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3870 [self waitForExpectations: @[operationRun] timeout:10];
3871 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3874 - (void)testCloudKitLogoutDueToGreyMode {
3875 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
3876 [self startCKKSSubsystem];
3877 [self performOctagonTLKUpload:self.ckksViews];
3878 XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "Should have been told of a 'login'");
3879 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:50*NSEC_PER_MSEC], "'logout' event should be reset");
3880 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3882 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
3884 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3885 [self waitForCKModifications];
3887 // simulate a cloudkit grey mode switch and NSNotification callback. CKKS should treat this as a logout
3888 self.iCloudHasValidCredentials = false;
3889 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3891 // Test that there are no items in the database after logout
3892 XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "Should have been told of a 'logout'");
3893 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:50*NSEC_PER_MSEC], "'login' event should be reset");
3894 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3895 [self checkNoCKKSData: self.keychainView];
3896 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
3898 // There should be no further uploads, even when we save keychain items
3899 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3900 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3902 [self.keychainView waitUntilAllOperationsAreFinished];
3903 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3905 // Also, fetches shouldn't occur
3906 self.silentFetchesAllowed = false;
3907 NSOperation* op = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
3908 CKKSResultOperation* timeoutOp = [CKKSResultOperation named:@"timeout" withBlock:^{}];
3909 [timeoutOp addDependency:op];
3910 [timeoutOp timeout:4*NSEC_PER_SEC];
3911 [self.operationQueue addOperation:timeoutOp];
3912 [timeoutOp waitUntilFinished];
3914 // CloudKit figures its life out. We expect the two passwords from before to be uploaded
3915 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
3916 self.silentFetchesAllowed = true;
3917 self.iCloudHasValidCredentials = true;
3918 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3920 XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "Should have been told of a 'login'");
3921 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:50*NSEC_PER_MSEC], "'logout' event should be reset");
3922 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3923 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3925 // And fetching still works!
3926 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D00" withAccount:@"account0"]];
3927 [self.keychainView notifyZoneChange:nil];
3928 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3929 [self findGenericPassword: @"account0" expecting:errSecSuccess];
3930 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3933 - (void)testCloudKitLoginRace {
3934 // Test starts with nothing in database, and 'in circle', but securityd hasn't received notification if we're logged into CloudKit.
3935 // CKKS should call handleLogout, as the CK account is not present.
3937 // note: don't unblock the ck account state object yet...
3939 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3940 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3942 // Add a keychain item, and make sure it doesn't upload yet.
3943 [self addGenericPassword: @"data" account: @"account-delete-me"];
3944 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3945 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'loggedout'");
3947 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3949 // Now that we're here (and logged out), bring the account up
3951 // We expect a single class C record to be uploaded.
3952 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3954 self.accountStatus = CKAccountStatusAvailable;
3955 [self startCKKSSubsystem];
3956 [self performOctagonTLKUpload:self.ckksViews];
3958 // simulate another NSNotification callback
3959 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3961 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3962 [self waitForCKModifications];
3964 // Make sure new items upload too
3965 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3966 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3967 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3969 [self.keychainView waitUntilAllOperationsAreFinished];
3970 [self waitForCKModifications];
3971 [self.keychainView halt];
3974 - (void)testDontLogOutIfBeforeFirstUnlock {
3976 // test starts as if a previously logged-in device has just rebooted
3977 self.aksLockState = true;
3978 self.accountStatus = CKAccountStatusAvailable;
3980 // This is the original state of the account tracker
3981 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCError error:nil];
3982 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3984 // And this is what the first circle status fetch will actually return
3985 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"]];
3986 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3988 XCTAssertEqual(self.accountStateTracker.currentComputedAccountStatus, CKKSAccountStatusUnknown, "Account tracker status should just be 'unknown'");
3989 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKKS should not yet know the CK account state");
3991 [self startCKKSSubsystem];
3993 XCTAssertEqual(0, [self.keychainView.loggedIn wait:8*NSEC_PER_SEC], "'login' event should have happened");
3994 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:10*NSEC_PER_MSEC], "Should not have been told of a CK 'logout' event on startup");
3995 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:1*NSEC_PER_SEC], "CKKS should know the account state");
3997 // And assume another CK status change
3998 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3999 XCTAssertEqual(self.accountStateTracker.currentComputedAccountStatus, CKKSAccountStatusUnknown, "Account tracker status should just be 'no account'");
4000 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKKS should know the CK account state");
4002 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
4004 self.aksLockState = false;
4006 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4007 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4008 [self beginSOSTrustedViewOperation:self.keychainView];
4010 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
4011 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
4012 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4014 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4015 [self waitForCKModifications];
4017 // We expect a single class C record to be uploaded.
4018 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4019 [self addGenericPassword: @"data" account: @"account-delete-me"];
4021 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4022 [self waitForCKModifications];*/
4025 - (void)testSyncableItemsAddedWhileLoggedOut {
4026 // Test that once CKKS is up and 'logged out', nothing happens when syncable items are added
4027 self.accountStatus = CKAccountStatusNoAccount;
4028 [self startCKKSSubsystem];
4030 XCTAssertEqual([self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], 0, "CKKS should be told that it's logged out");
4032 // CKKS shouldn't decide to poke its state machine, but it should still send the notification
4033 XCTestExpectation* viewChangeNotification = [self expectChangeForView:self.keychainZoneID.zoneName];
4035 // Reject all attempts to trigger a state machine update
4036 id pokeKeyStateMachineScheduler = OCMClassMock([CKKSNearFutureScheduler class]);
4037 OCMReject([pokeKeyStateMachineScheduler trigger]);
4038 self.keychainView.pokeKeyStateMachineScheduler = pokeKeyStateMachineScheduler;
4040 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4042 [self waitForExpectations:@[viewChangeNotification] timeout:8];
4043 [pokeKeyStateMachineScheduler stopMocking];
4046 - (void)testUploadSyncableItemsAddedWhileUntrusted {
4047 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4048 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4050 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
4052 [self startCKKSSubsystem];
4054 XCTAssertEqual([self.keychainView.loggedIn wait:500*NSEC_PER_MSEC], 0, "CKKS should be told that it's logged in");
4056 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered waitfortrust");
4057 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4059 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4063 NSError* error = nil;
4064 NSDictionary* currentOQEs = [CKKSOutgoingQueueEntry countsByStateInZone:self.keychainZoneID error:&error];
4065 XCTAssertNil(error, "Should be no error coutning OQEs");
4066 XCTAssertEqual(0, currentOQEs.count, "Should be no OQEs");
4068 // Now, insert a restart to simulate securityd restarting (and throwing away all pending operations), then a real sign in
4069 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4070 [self endSOSTrustedViewOperation:self.keychainView];
4071 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered waitfortrust");
4073 // Okay! Upon sign in, this item should be uploaded
4074 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4075 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4077 [self putSelfTLKSharesInCloudKit:self.keychainZoneID];
4078 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4079 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4080 [self beginSOSTrustedViewOperation:self.keychainView];
4082 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4083 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4086 // Note that this test assumes that the keychainView object was created at daemon restart.
4087 // I don't really know how to write a test for that...
4088 - (void)testSyncableItemAddedOnDaemonRestartBeforeCloudKitAccountKnown {
4089 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
4090 [self startCKKSSubsystem];
4092 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4095 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4096 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4097 [self beginSOSTrustedViewOperation:self.keychainView];
4099 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4100 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:100*NSEC_PER_MSEC], "CKKS should still have no idea what the account state is");
4101 XCTAssertEqual(self.keychainView.accountStatus, CKKSAccountStatusUnknown, "Account status should be unknown");
4102 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
4104 [self.keychainView beginCloudKitOperation];
4106 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4107 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4108 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4109 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4112 - (void)testSyncableItemModifiedOnDaemonRestartBeforeCloudKitAccountKnown {
4113 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
4114 [self startCKKSSubsystem];
4116 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4118 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4119 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4120 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4121 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4124 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4125 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4126 [self beginSOSTrustedViewOperation:self.keychainView];
4128 [self updateGenericPassword:@"newdata" account: @"account-delete-me-2"];
4129 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:100*NSEC_PER_MSEC], "CKKS should still have no idea what the account state is");
4130 XCTAssertEqual(self.keychainView.accountStatus, CKKSAccountStatusUnknown, "Account status should be unknown");
4131 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
4133 [self.keychainView beginCloudKitOperation];
4135 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4136 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4137 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4138 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4141 - (void)testSyncableItemDeletedOnDaemonRestartBeforeCloudKitAccountKnown {
4142 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
4143 [self startCKKSSubsystem];
4145 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4147 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4148 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4149 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4150 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4153 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4154 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4155 [self beginSOSTrustedViewOperation:self.keychainView];
4157 [self deleteGenericPassword:@"account-delete-me-2"];
4158 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:100*NSEC_PER_MSEC], "CKKS should still have no idea what the account state is");
4159 XCTAssertEqual(self.keychainView.accountStatus, CKKSAccountStatusUnknown, "Account status should be unknown");
4160 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
4162 [self.keychainView beginCloudKitOperation];
4164 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
4165 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4166 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4169 - (void)testNotStuckAfterReset {
4170 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
4172 XCTestExpectation *operationRun = [self expectationWithDescription:@"operation run"];
4173 NSOperation* op = [NSBlockOperation named:@"test" withBlock:^{
4174 [operationRun fulfill];
4177 [op addDependency:self.keychainView.keyStateReadyDependency];
4178 [self.operationQueue addOperation:op];
4180 // And handle a spurious logout
4181 [self.keychainView handleCKLogout];
4183 [self startCKKSSubsystem];
4185 [self waitForExpectations: @[operationRun] timeout:20];
4188 - (void)testCKKSControlBringup {
4189 NSXPCInterface *interface = CKKSSetupControlProtocol([NSXPCInterface interfaceWithProtocol:@protocol(CKKSControlProtocol)]);
4190 XCTAssertNotNil(interface, "Received a configured CKKS interface");
4193 - (void)testMetricsUpload {
4195 XCTestExpectation *upload = [self expectationWithDescription:@"CAMetrics"];
4196 XCTestExpectation *collection = [self expectationWithDescription:@"CAMetrics"];
4198 id saMock = OCMClassMock([SecCoreAnalytics class]);
4199 OCMStub([saMock sendEvent:[OCMArg any] event:[OCMArg any]]).andDo(^(NSInvocation* invocation) {
4203 NSString *sampleSampler = @"stuff";
4205 [[CKKSAnalytics logger] AddMultiSamplerForName:sampleSampler withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSDictionary<NSString *,NSNumber *> *{
4206 [collection fulfill];
4207 return @{ @"hej" : @1 };
4211 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
4212 [self startCKKSSubsystem];
4214 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
4216 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
4217 deletedRecordTypeCounts:nil
4218 zoneID:self.keychainZoneID
4219 checkModifiedRecord:nil
4220 runAfterModification:nil];
4222 [self.injectedManager xpc24HrNotification];
4224 [self waitForExpectations: @[upload, collection] timeout:10];
4225 [[CKKSAnalytics logger] removeMultiSamplerForName:sampleSampler];
4228 - (void)testSaveManyTLKShares {
4229 // Spin up CKKS subsystem.
4230 [self startCKKSSubsystem];
4232 [self performOctagonTLKUpload:self.ckksViews];
4233 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4235 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
4237 NSMutableArray<CKKSSOSSelfPeer*>* peers = [NSMutableArray array];
4239 for(int i = 0; i < 20; i++) {
4240 CKKSSOSSelfPeer* untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:[NSString stringWithFormat:@"untrusted-peer-%d", i]
4241 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
4242 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
4243 viewList:self.managedViewList];
4245 [peers addObject:untrustedPeer];
4248 NSMutableArray<CKRecord*>* tlkShareRecords = [NSMutableArray array];
4250 for(CKKSSOSSelfPeer* peer1 in peers) {
4251 for(CKKSSOSSelfPeer* peer2 in peers) {
4252 NSError* error = nil;
4253 CKKSTLKShareRecord* share = [CKKSTLKShareRecord share:self.keychainZoneKeys.tlk
4259 XCTAssertNil(error, "Should have been no error sharing a CKKSKey");
4260 XCTAssertNotNil(share, "Should be able to create a share");
4262 CKRecord* shareRecord = [share CKRecordWithZoneID:self.keychainZoneID];
4263 [tlkShareRecords addObject:shareRecord];
4267 [self measureBlock:^{
4268 [self.keychainView dispatchSyncWithAccountKeys:^bool{
4269 for(CKRecord* record in tlkShareRecords) {
4270 [self.keychainView _onqueueCKRecordChanged:record resync:false];