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:@"50184A35-4480-E8BA-769B-567CF72F1EC0" 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");
743 -(void)testReceiveConflictOnJustAddedItem {
744 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
745 [self startCKKSSubsystem];
747 [self.keychainView waitForKeyHierarchyReadiness];
748 [self.keychainView waitUntilAllOperationsAreFinished];
750 // Place a hold on processing the outgoing queue.
751 CKKSResultOperation* blockOutgoing = [CKKSResultOperation operationWithBlock:^{
752 secnotice("ckks", "Outgoing queue hold released.");
754 blockOutgoing.name = @"outgoing-queue-hold";
755 CKKSResultOperation* outgoingQueueOperation = [self.keychainView processOutgoingQueueAfter:blockOutgoing ckoperationGroup:nil];
757 CKKSResultOperation* blockIncoming = [CKKSResultOperation operationWithBlock:^{
758 secnotice("ckks", "Incoming queue hold released.");
760 blockIncoming.name = @"incoming-queue-hold";
761 CKKSResultOperation* incomingQueueOperation = [self.keychainView processIncomingQueue:false after: blockIncoming];
763 [self addGenericPassword:@"localchange" account:@"account-delete-me"];
765 // Pull out the new item's UUID.
766 __block NSString* itemUUID = nil;
767 [self.keychainView dispatchSync:^bool {
768 NSError* error = nil;
769 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID ?: [[CKRecordZoneID alloc] initWithZoneName:@"keychain"
770 ownerName:CKCurrentUserDefaultName]
772 XCTAssertNil(error, "no error fetching uuids");
773 XCTAssertEqual(uuids.count, 1u, "There's exactly one outgoing queue entry");
776 XCTAssertNotNil(itemUUID, "Have a UUID for our new item");
780 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName: itemUUID]];
782 [self.keychainView notifyZoneChange:nil];
783 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
785 // Allow the outgoing queue operation to proceed
786 [self.operationQueue addOperation:blockOutgoing];
787 [outgoingQueueOperation waitUntilFinished];
789 // Allow the incoming queue operation to proceed
790 [self.operationQueue addOperation:blockIncoming];
791 [incomingQueueOperation waitUntilFinished];
793 [self checkGenericPassword:@"data" account:@"account-delete-me"];
795 [self.keychainView waitUntilAllOperationsAreFinished];
798 - (void)testReceiveCloudKitConflictOnJustAddedItems {
799 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
800 [self startCKKSSubsystem];
802 [self.keychainView waitForKeyHierarchyReadiness];
803 [self.keychainView waitUntilAllOperationsAreFinished];
805 // Place a hold on processing the outgoing queue.
806 self.keychainView.holdOutgoingQueueOperation = [CKKSResultOperation named:@"outgoing-queue-hold" withBlock:^{
807 secnotice("ckks", "Outgoing queue hold released.");
810 [self addGenericPassword:@"localchange" account:@"account-delete-me"];
812 // Pull out the new item's UUID.
813 __block NSString* itemUUID = nil;
814 [self.keychainView dispatchSync:^bool {
815 NSError* error = nil;
816 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID ?: [[CKRecordZoneID alloc] initWithZoneName:@"keychain"
817 ownerName:CKCurrentUserDefaultName]
819 XCTAssertNil(error, "no error fetching uuids");
820 XCTAssertEqual(uuids.count, 1u, "There's exactly one outgoing queue entry");
823 XCTAssertNotNil(itemUUID, "Have a UUID for our new item");
827 // Add a second item: this item should be uploaded after the failure of the first item
828 [self addGenericPassword:@"localchange" account:@"account-delete-me-2"];
830 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName: itemUUID]];
832 // Also, this write will increment the class C current pointer's etag
833 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
834 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
835 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
836 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
837 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
839 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
840 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
841 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
843 // Allow the outgoing queue operation to proceed
844 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
846 OCMVerifyAllWithDelay(self.mockDatabase, 20);
847 [self.keychainView waitUntilAllOperationsAreFinished];
849 [self checkGenericPassword:@"data" account:@"account-delete-me"];
850 [self checkGenericPassword:@"localchange" account:@"account-delete-me-2"];
854 -(void)testReceiveUnknownField {
855 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
857 [self startCKKSSubsystem];
858 [self.keychainView waitForKeyHierarchyReadiness];
860 NSError* error = nil;
862 // Manually encrypt an item
863 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
864 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
865 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
866 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
867 parentKeyUUID:self.keychainZoneKeys.classA.uuid
868 zoneID:recordID.zoneID];
869 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classA error:&error];
870 XCTAssertNotNil(itemkey, "Got a key");
871 cipheritem.wrappedkey = itemkey.wrappedkey;
872 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
874 NSData* future_data_field = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding];
875 NSString* future_string_field = @"authstring";
876 NSString* future_server_field = @"server_can_change_at_any_time";
877 NSNumber* future_number_field = [NSNumber numberWithInt:30];
879 // Use version 2, so future fields will be authenticated
880 cipheritem.encver = CKKSItemEncryptionVersion2;
881 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
883 authenticatedData[@"future_data_field"] = future_data_field;
884 authenticatedData[@"future_string_field"] = [future_string_field dataUsingEncoding:NSUTF8StringEncoding];
886 uint64_t n = OSSwapHostToLittleConstInt64([future_number_field unsignedLongValue]);
887 authenticatedData[@"future_number_field"] = [NSData dataWithBytes:&n length:sizeof(n)];
890 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
891 XCTAssertNil(error, "no error encrypting object");
892 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
894 CKRecord* ckr = [cipheritem CKRecordWithZoneID: recordID.zoneID];
895 ckr[@"future_data_field"] = future_data_field;
896 ckr[@"future_string_field"] = future_string_field;
897 ckr[@"future_number_field"] = future_number_field;
898 ckr[@"server_new_server_field"] = future_server_field;
899 [self.keychainZone addToZone:ckr];
901 [self.keychainView waitForFetchAndIncomingQueueProcessing];
903 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
904 (id)kSecReturnAttributes: @YES,
905 (id)kSecAttrSynchronizable: @YES,
906 (id)kSecAttrAccount: @"account-delete-me",
907 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
909 CFTypeRef cfresult = NULL;
910 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
912 // Test that if this item is updated, it remains encrypted in v2, and future_field still exists
913 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
914 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
916 OCMVerifyAllWithDelay(self.mockDatabase, 20);
917 [self waitForCKModifications];
919 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
920 XCTAssertEqualObjects(newRecord[@"future_data_field"], future_data_field, "future_data_field still exists");
921 XCTAssertEqualObjects(newRecord[@"future_string_field"], future_string_field, "future_string_field still exists");
922 XCTAssertEqualObjects(newRecord[@"future_number_field"], future_number_field, "future_string_field still exists");
923 XCTAssertEqualObjects(newRecord[@"server_new_server_field"], future_server_field, "future_server_field stille exists");
925 CKKSItem* newItem = [[CKKSItem alloc] initWithCKRecord:newRecord];
926 CKKSAESSIVKey* newItemKey = [self.keychainZoneKeys.classA unwrapAESKey:newItem.wrappedkey error:&error];
927 XCTAssertNil(error, "No error unwrapping AES key");
928 XCTAssertNotNil(newItemKey, "Have an unwrapped AES key for this item");
930 NSDictionary* uploadedData = [CKKSItemEncrypter decryptDictionary:newRecord[SecCKRecordDataKey]
932 authenticatedData:authenticatedData
934 XCTAssertNil(error, "No error decrypting dictionary");
935 XCTAssertNotNil(uploadedData, "Authenticated re-uploaded data including future_field");
936 XCTAssertEqualObjects(uploadedData[@"v_Data"], [@"different password" dataUsingEncoding:NSUTF8StringEncoding], "Passwords match");
940 -(void)testReceiveRecordEncryptedv1 {
941 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
943 [self startCKKSSubsystem];
944 [self.keychainView waitForKeyHierarchyReadiness];
946 NSError* error = nil;
948 // Manually encrypt an item
949 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
950 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
951 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
952 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
953 parentKeyUUID:self.keychainZoneKeys.classC.uuid
954 zoneID:recordID.zoneID];
955 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
956 XCTAssertNotNil(itemkey, "Got a key");
957 cipheritem.wrappedkey = itemkey.wrappedkey;
958 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
960 cipheritem.encver = CKKSItemEncryptionVersion1;
962 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:cipheritem.encver] mutableCopy];
964 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
965 XCTAssertNil(error, "no error encrypting object");
966 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
968 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
970 [self.keychainView waitForFetchAndIncomingQueueProcessing];
972 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
973 (id)kSecReturnAttributes: @YES,
974 (id)kSecAttrSynchronizable: @YES,
975 (id)kSecAttrAccount: @"account-delete-me",
976 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
978 CFTypeRef cfresult = NULL;
979 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
980 CFReleaseNull(cfresult);
982 // Test that if this item is updated, it is encrypted in v2
983 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
984 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
986 OCMVerifyAllWithDelay(self.mockDatabase, 20);
987 [self waitForCKModifications];
989 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
990 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
993 - (void)testUploadPagination {
994 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
996 for(size_t count = 0; count < 250; count++) {
997 [self addGenericPassword: @"data" account: [NSString stringWithFormat:@"account-delete-me-%03lu", count]];
1000 [self startCKKSSubsystem];
1002 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1003 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1004 [self expectCKModifyItemRecords: 50 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1006 OCMVerifyAllWithDelay(self.mockDatabase, 40);
1009 - (void)testUploadInitialKeyHierarchy {
1010 // Test starts with nothing in database. CKKS should get into the "please upload my keys" state, then Octagon should perform the upload
1012 // Spin up CKKS subsystem.
1013 [self startCKKSSubsystem];
1015 [self performOctagonTLKUpload:self.ckksViews];
1016 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1018 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
1021 - (void)testDoNotErrorIfNudgedWhileWaitingForTLKUpload {
1022 // Test starts with nothing in database. CKKS should get into the "please upload my keys" state, then Octagon should perform the upload
1024 // Spin up CKKS subsystem.
1025 [self startCKKSSubsystem];
1027 NSMutableArray<CKKSResultOperation<CKKSKeySetProviderOperationProtocol>*>* keysetOps = [NSMutableArray array];
1029 for(CKKSKeychainView* view in self.ckksViews) {
1030 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:40*NSEC_PER_SEC], @"key state should enter 'waitfortlkcreation' (view %@)", view);
1031 [keysetOps addObject: [view findKeySet]];
1034 // Now that we've kicked them all off, wait for them to resolve (and nudge each one, as if a key was saved)
1035 for(CKKSKeychainView* view in self.ckksViews) {
1036 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKUpload] wait:40*NSEC_PER_SEC], @"key state should enter 'waitfortlkupload'");
1038 CKKSCondition* viewProcess = view.keyHierarchyConditions[SecCKKSZoneKeyStateProcess];
1039 [view keyStateMachineRequestProcess];
1040 XCTAssertNotEqual(0, [viewProcess wait:500*NSEC_PER_MSEC], "CKKS should not reprocess the key hierarchy, even if nudged");
1043 // The views should remain in waitfortlkcreation, and not go through process into an error
1045 NSMutableArray<CKRecord*>* keyHierarchyRecords = [NSMutableArray array];
1047 for(CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp in keysetOps) {
1048 // Wait until finished is usually a bad idea. We could rip this out into an operation if we'd like.
1049 [keysetOp waitUntilFinished];
1050 XCTAssertNil(keysetOp.error, "Should be no error fetching keyset from CKKS");
1052 NSArray<CKRecord*>* records = [self putKeySetInCloudKit:keysetOp.keyset];
1053 [keyHierarchyRecords addObjectsFromArray:records];
1056 // Tell our views about our shiny new records!
1057 for(CKKSKeychainView* view in self.ckksViews) {
1058 [view receiveTLKUploadRecords: keyHierarchyRecords];
1060 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1062 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
1065 - (void)testProvideKeysetFromNoTrust {
1067 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1068 [self startCKKSSubsystem];
1070 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlkcreation'");
1071 // I'm not sure how CKKS ends up in 'waitfortrust' without a keyset, so force that state
1072 // In 52301278, it occurred with some complex interaction of zone deletions, fetches, and trust operations
1073 [self.keychainView dispatchSyncWithAccountKeys:^bool{
1074 [self.keychainView _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateWaitForTrust withError:nil];
1077 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortrust'");
1079 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet];
1080 [keysetOp timeout:20*NSEC_PER_SEC];
1081 [keysetOp waitUntilFinished];
1083 XCTAssertNil(keysetOp.error, "Should be no error fetching a keyset");
1086 // This test no longer is very interesting, since Octagon needs to handle lock states, not CKKS...
1087 - (void)testUploadInitialKeyHierarchyAfterLockedStart {
1088 // 'Lock' the keybag
1089 self.aksLockState = true;
1090 [self.lockStateTracker recheck];
1092 [self startCKKSSubsystem];
1094 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1095 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitfortlkcreation");
1097 // After unlock, the key hierarchy should be created.
1098 self.aksLockState = false;
1099 [self.lockStateTracker recheck];
1101 [self performOctagonTLKUpload:self.ckksViews];
1103 // We expect a single class C record to be uploaded.
1104 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1106 [self addGenericPassword: @"data" account: @"account-delete-me"];
1107 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1110 - (void)testLockImmediatelyAfterUploadingInitialKeyHierarchy {
1112 __weak __typeof(self) weakSelf = self;
1114 [self startCKKSSubsystem];
1115 [self performOctagonTLKUpload:self.ckksViews afterUpload:^{
1116 __strong __typeof(self) strongSelf = weakSelf;
1117 [strongSelf holdCloudKitFetches];
1120 // Should enter 'ready'
1121 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1122 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1124 // Now, lock and allow fetches again
1125 self.aksLockState = true;
1126 [self.lockStateTracker recheck];
1127 [self releaseCloudKitFetchHold];
1129 CKKSResultOperation* op = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
1130 [op waitUntilFinished];
1132 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1134 // Wait for CKKS to shake itself out...
1135 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
1137 // Should be in ReadyPendingUnlock
1138 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1140 // We expect a single class C record to be uploaded.
1141 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1142 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1144 [self addGenericPassword: @"data" account: @"account-delete-me"];
1145 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1148 - (void)testReceiveKeyHierarchyAfterLockedStart {
1149 // 'Lock' the keybag
1150 self.aksLockState = true;
1151 [self.lockStateTracker recheck];
1153 [self startCKKSSubsystem];
1155 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1156 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitfortlkcreation");
1158 // Now, another device comes along and creates the hierarchy; we download it; and it and sends us the TLK
1159 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1160 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1161 [self.keychainView notifyZoneChange:nil];
1162 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
1164 self.aksLockState = false;
1165 [self.lockStateTracker recheck];
1166 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should end up in waitfortlk");
1168 // After unlock, the TLK arrives
1169 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1170 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1172 // We expect a single class C record to be uploaded.
1173 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1175 [self addGenericPassword: @"data" account: @"account-delete-me"];
1176 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1179 - (void)testLoadKeyHierarchyAfterLockedStart {
1180 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
1182 // 'Lock' the keybag
1183 self.aksLockState = true;
1184 [self.lockStateTracker recheck];
1186 [self startCKKSSubsystem];
1188 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1189 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1191 self.aksLockState = false;
1192 [self.lockStateTracker recheck];
1194 // We expect a single class C record to be uploaded.
1195 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1197 [self addGenericPassword: @"data" account: @"account-delete-me"];
1198 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1201 - (void)testUploadAndUseKeyHierarchy {
1202 [self startCKKSSubsystem];
1203 [self performOctagonTLKUpload:self.ckksViews];
1205 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
1206 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1207 (id)kSecAttrAccount : @"account-delete-me",
1208 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1209 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1211 CFTypeRef item = NULL;
1212 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not exist");
1214 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1215 [self waitForCKModifications];
1217 // We expect a single class C record to be uploaded.
1218 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1220 [self addGenericPassword: @"data" account: @"account-delete-me"];
1221 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1223 // now, expect a single class A record to be uploaded
1224 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1226 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef)@{
1227 (id)kSecClass : (id)kSecClassGenericPassword,
1228 (id)kSecAttrAccessGroup : @"com.apple.security.sos",
1229 (id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked,
1230 (id)kSecAttrAccount : @"account-class-A",
1231 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1232 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
1233 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
1234 }, NULL), @"Adding class A item");
1235 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1238 - (void)testUploadInitialKeyHierarchyTriggersBackup {
1239 // We also expect the view manager's notifyNewTLKsInKeychain call to fire (after some delay)
1240 OCMExpect([self.mockCKKSViewManager notifyNewTLKsInKeychain]);
1242 // Spin up CKKS subsystem.
1243 [self startCKKSSubsystem];
1244 [self performOctagonTLKUpload:self.ckksViews];
1246 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1247 OCMVerifyAllWithDelay(self.mockCKKSViewManager, 10);
1250 - (void)testResetCloudKitZoneFromNoTLK {
1251 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1252 OCMExpect([self.suggestTLKUpload trigger]);
1254 self.silentZoneDeletesAllowed = true;
1256 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1257 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1258 // explicitly do not save a fake device status here
1259 self.keychainZone.flag = true;
1261 [self startCKKSSubsystem];
1262 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1264 // But then, it'll fire off the reset and reach 'ready', with a little help from octagon
1265 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1266 [self performOctagonTLKUpload:self.ckksViews];
1268 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1269 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1271 // And the zone should have been cleared and re-made
1272 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1275 - (void)testResetCloudKitZoneFromNoTLKWithOtherWaitForTLKDevices {
1276 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1277 OCMExpect([self.suggestTLKUpload trigger]);
1279 self.silentZoneDeletesAllowed = true;
1281 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1282 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1283 // Save a fake device status here, but modify its key state to be 'waitfortlk': it has no idea what the TLK is either
1284 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1285 [self putFakeOctagonOnlyDeviceStatusInCloudKit:self.keychainZoneID];
1287 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1288 if([record.recordType isEqualToString:SecCKRecordDeviceStateType]) {
1289 record[SecCKRecordKeyState] = CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTLK);
1293 self.keychainZone.flag = true;
1295 [self startCKKSSubsystem];
1296 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1298 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1299 [self performOctagonTLKUpload:self.ckksViews];
1301 // But then, it'll fire off the reset and reach 'ready'
1302 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1303 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1305 // And the zone should have been cleared and re-made
1306 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1309 - (void)testResetCloudKitZoneFromNoTLKIgnoringInactiveDevices {
1310 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1311 OCMExpect([self.suggestTLKUpload trigger]);
1313 self.silentZoneDeletesAllowed = true;
1315 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1316 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1317 // Save a fake device status here, but modify its creation and modification times to be months ago
1318 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1319 [self putFakeOctagonOnlyDeviceStatusInCloudKit:self.keychainZoneID];
1321 // Put a 'in-circle' TLKShare record, but also modify its creation and modification times
1322 CKKSSOSSelfPeer* untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"untrusted-peer"
1323 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1324 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1325 viewList:self.managedViewList];
1326 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:untrustedPeer to:untrustedPeer zoneID:self.keychainZoneID];
1328 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1329 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1330 record.creationDate = [NSDate distantPast];
1331 record.modificationDate = [NSDate distantPast];
1335 self.keychainZone.flag = true;
1337 [self startCKKSSubsystem];
1338 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1340 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1341 [self performOctagonTLKUpload:self.ckksViews];
1343 // But then, it'll fire off the reset and reach 'ready'
1344 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1345 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1347 // And the zone should have been cleared and re-made
1348 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1351 - (void)testDoNotResetCloudKitZoneDuringBadCircleState {
1352 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1353 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1355 // This test has stuff in CloudKit, but no TLKs.
1356 // CKKS should NOT reset the CK zone.
1357 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1358 self.zones[self.keychainZoneID].flag = true;
1360 [self startCKKSSubsystem];
1362 // But since we're out of circle, this test needs to initialize the zone itself
1363 [self.keychainView beginCloudKitOperation];
1365 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered waitfortrust");
1366 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1368 FakeCKZone* keychainZone = self.zones[self.keychainZoneID];
1369 XCTAssertNotNil(keychainZone, "Should still have a keychain zone");
1370 XCTAssertTrue(keychainZone.flag, "keychain zone should not have been recreated");
1373 - (void)testDoNotResetCloudKitZoneFromWaitForTLKDueToRecentDeviceState {
1374 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1376 // CKKS shouldn't reset this zone, due to a recent device status claiming to have TLKs
1377 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1379 // Also, CKKS _should_ be able to return the key hierarchy if asked before it starts
1380 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet];
1382 NSDateComponents* offset = [[NSDateComponents alloc] init];
1384 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1385 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1386 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1387 record.creationDate = updateTime;
1388 record.modificationDate = updateTime;
1392 self.keychainZone.flag = true;
1393 [self startCKKSSubsystem];
1395 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1397 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1399 // And, ensure that the keyset op ran and has results
1400 CKKSResultOperation* waitOp = [CKKSResultOperation named:@"test op" withBlock:^{}];
1401 [waitOp addDependency:keysetOp];
1402 [waitOp timeout:2*NSEC_PER_SEC];
1403 [self.operationQueue addOperation:waitOp];
1404 [waitOp waitUntilFinished];
1406 XCTAssert(keysetOp.finished, "Keyset op should have finished");
1407 XCTAssertNil(keysetOp.error, "keyset op should not have errored");
1408 XCTAssertNotNil(keysetOp.keyset, "keyset op should have a keyset");
1409 XCTAssertNotNil(keysetOp.keyset.currentTLKPointer, "keyset should have a current TLK pointer");
1410 XCTAssertEqualObjects(keysetOp.keyset.currentTLKPointer.currentKeyUUID, self.keychainZoneKeys.tlk.uuid, "keyset should match what's in zone");
1413 - (void)testDoNotCloudKitZoneFromWaitForTLKDueToRecentButUntrustedDeviceState {
1414 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1416 // CKKS should reset this zone, even though to a recent device status claiming to have TLKs. The device isn't trusted
1417 self.silentZoneDeletesAllowed = true;
1418 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1419 [self.mockSOSAdapter.trustedPeers removeObject:self.remoteSOSOnlyPeer];
1421 self.keychainZone.flag = true;
1422 [self startCKKSSubsystem];
1424 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1425 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1427 // And ensure it doesn't go on to 'reset'
1428 XCTAssertNotEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:100*NSEC_PER_MSEC], @"Key state should not become 'resetzone'");
1431 - (void)testResetCloudKitZoneFromWaitForTLKDueToLessRecentAndUntrustedDeviceState {
1432 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1433 OCMExpect([self.suggestTLKUpload trigger]);
1435 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1437 // CKKS should reset this zone, even though to a recent device status claiming to have TLKs. The device isn't trusted
1438 self.silentZoneDeletesAllowed = true;
1439 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1440 [self.mockSOSAdapter.trustedPeers removeObject:self.remoteSOSOnlyPeer];
1442 NSDateComponents* offset = [[NSDateComponents alloc] init];
1444 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1445 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1446 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1447 record.creationDate = updateTime;
1448 record.modificationDate = updateTime;
1452 self.keychainZone.flag = true;
1453 [self startCKKSSubsystem];
1454 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1456 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1457 [self performOctagonTLKUpload:self.ckksViews];
1459 // Then we should reset.
1460 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1461 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1463 // And the zone should have been cleared and re-made
1464 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1467 - (void)testAcceptExistingKeyHierarchy {
1468 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
1469 // Test also begins with the TLK having arrived in the local keychain (via SOS)
1470 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1471 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1472 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1474 // Spin up CKKS subsystem.
1475 [self startCKKSSubsystem];
1477 // The CKKS subsystem should only upload its TLK share
1478 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1480 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1482 // Verify that there are three local keys, and three local current key records
1483 __weak __typeof(self) weakSelf = self;
1484 [self.keychainView dispatchSync: ^bool{
1485 __strong __typeof(weakSelf) strongSelf = weakSelf;
1486 XCTAssertNotNil(strongSelf, "self exists");
1488 NSError* error = nil;
1490 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
1491 XCTAssertNil(error, "no error fetching keys");
1492 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
1494 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1495 XCTAssertNil(error, "no error fetching current keys");
1496 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1502 - (void)testAcceptExistingAndUseKeyHierarchy {
1503 // Test starts with nothing in database, but one in our fake CloudKit.
1504 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1505 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1506 // But, CKKS shouldn't ever reset the zone
1507 self.keychainZone.flag = true;
1509 [self startCKKSSubsystem];
1510 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:200*NSEC_PER_SEC], "Key state should have become waitfortlk");
1512 // Now, save the TLK to the keychain (to simulate it coming in later via SOS). We'll create a TLK share for ourselves.
1513 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1514 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1516 // Wait for the key hierarchy to sort itself out, to make it easier on this test; see testOnboardOldItemsWithExistingKeyHierarchy for the other test.
1517 // The CKKS subsystem should write its TLK share, but nothing else
1518 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1520 // We expect a single record to be uploaded for each key class
1521 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1522 [self addGenericPassword: @"data" account: @"account-delete-me"];
1523 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1525 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1526 [self addGenericPassword:@"asdf"
1527 account:@"account-class-A"
1529 access:(id)kSecAttrAccessibleWhenUnlocked
1530 expecting:errSecSuccess
1531 message:@"Adding class A item"];
1532 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1533 XCTAssertTrue(self.keychainZone.flag, "Keychain zone shouldn't have been reset");
1536 - (void)testAcceptExistingKeyHierarchyDespiteLocked {
1537 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
1538 // Test also begins with the TLK having arrived in the local keychain (via SOS)
1540 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1541 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1543 self.aksLockState = true;
1544 [self.lockStateTracker recheck];
1546 // Spin up CKKS subsystem.
1547 [self startCKKSSubsystem];
1549 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:20*NSEC_PER_SEC], "Key state should have become waitforunlock");
1551 // CKKS will give itself a TLK Share
1552 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1554 // Now that all operations are complete, 'unlock' AKS
1555 self.aksLockState = false;
1556 [self.lockStateTracker recheck];
1558 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1559 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1561 // Verify that there are three local keys, and three local current key records
1562 __weak __typeof(self) weakSelf = self;
1563 [self.keychainView dispatchSync: ^bool{
1564 __strong __typeof(weakSelf) strongSelf = weakSelf;
1565 XCTAssertNotNil(strongSelf, "self exists");
1567 NSError* error = nil;
1569 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
1570 XCTAssertNil(error, "no error fetching keys");
1571 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
1573 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1574 XCTAssertNil(error, "no error fetching current keys");
1575 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1581 - (void)testReceiveClassCWhileALocked {
1582 // Test starts with a key hierarchy already existing.
1583 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
1584 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1585 [self startCKKSSubsystem];
1587 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1588 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1590 [self findGenericPassword:@"classCItem" expecting:errSecItemNotFound];
1591 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
1593 // 'Lock' the keybag
1594 self.aksLockState = true;
1595 [self.lockStateTracker recheck];
1597 XCTAssertNotNil(self.keychainZoneKeys, "Have zone keys for zone");
1598 XCTAssertNotNil(self.keychainZoneKeys.classA, "Have class A key for zone");
1599 XCTAssertNotNil(self.keychainZoneKeys.classC, "Have class C key for zone");
1601 [self.keychainView dispatchSyncWithAccountKeys: ^bool {
1602 [self.keychainView _onqueueKeyStateMachineRequestProcess];
1605 // And ensure we end up back in 'readypendingunlock': we have the keys, we're just locked now
1606 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
1607 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1609 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"classCItem" key:self.keychainZoneKeys.classC]];
1610 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-FFFF-FFFF-FFFF-5A507ACB2D85" withAccount:@"classAItem" key:self.keychainZoneKeys.classA]];
1612 CKKSResultOperation* op = [self.keychainView waitForFetchAndIncomingQueueProcessing];
1613 // The processing op should NOT error, even though it didn't manage to process the classA item
1614 XCTAssertNil(op.error, "no error while failing to process a class A item");
1616 CKKSResultOperation* erroringOp = [self.keychainView processIncomingQueue:true];
1617 [erroringOp waitUntilFinished];
1618 XCTAssertNotNil(erroringOp.error, "error exists while processing a class A item");
1620 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
1621 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
1623 self.aksLockState = false;
1624 [self.lockStateTracker recheck];
1625 [self.keychainView waitUntilAllOperationsAreFinished];
1627 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
1628 [self findGenericPassword:@"classAItem" expecting:errSecSuccess];
1631 - (void)testRestartWhileLocked {
1632 [self startCKKSSubsystem];
1633 [self performOctagonTLKUpload:self.ckksViews];
1635 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1637 // 'Lock' the keybag
1638 self.aksLockState = true;
1639 [self.lockStateTracker recheck];
1641 [self.keychainView halt];
1642 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1643 [self.keychainView beginCloudKitOperation];
1644 [self beginSOSTrustedViewOperation:self.keychainView];
1646 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1648 self.aksLockState = false;
1649 [self.lockStateTracker recheck];
1651 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1654 - (void)testExternalKeyRoll {
1655 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1656 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1657 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1658 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1660 // Spin up CKKS subsystem.
1661 [self startCKKSSubsystem];
1663 // The CKKS subsystem should not try to write anything to the CloudKit database.
1664 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1666 __weak __typeof(self) weakSelf = self;
1668 // We expect a single record to be uploaded.
1669 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1671 [self addGenericPassword: @"data" account: @"account-delete-me"];
1673 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1674 [self waitForCKModifications];
1676 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1677 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1678 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1680 // Trigger a notification
1681 [self.keychainView notifyZoneChange:nil];
1683 // Make life easy on this test; testAcceptKeyConflictAndUploadReencryptedItem will check the case when we don't receive the notification
1684 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1686 // Just in extra case of threading issues, force a reexamination of the key hierarchy
1687 [self.keychainView dispatchSyncWithAccountKeys: ^bool {
1688 [self.keychainView _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
1692 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1694 // Verify that there are six local keys, and three local current key records
1695 [self.keychainView dispatchSync: ^bool{
1696 __strong __typeof(weakSelf) strongSelf = weakSelf;
1697 XCTAssertNotNil(strongSelf, "self exists");
1699 NSError* error = nil;
1700 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:self.keychainZoneID error:&error];
1701 XCTAssertNil(error, "no error fetching keys");
1702 XCTAssertEqual(keys.count, 6u, "Six keys in local database");
1704 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1705 XCTAssertNil(error, "no error fetching current keys");
1706 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1708 for(CKKSCurrentKeyPointer* key in currentkeys) {
1709 if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
1710 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.tlk.uuid);
1711 } else if([key.keyclass isEqualToString: SecCKKSKeyClassA]) {
1712 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classA.uuid);
1713 } else if([key.keyclass isEqualToString: SecCKKSKeyClassC]) {
1714 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classC.uuid);
1716 XCTFail("Unknown key class: %@", key.keyclass);
1723 // We expect a single record to be uploaded.
1724 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1726 // TODO: remove this by writing code for item reencrypt after key arrival
1727 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1729 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1731 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1734 - (void)testAcceptKeyConflictAndUploadReencryptedItem {
1735 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1736 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1737 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1738 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1740 [self startCKKSSubsystem];
1741 [self.keychainView waitUntilAllOperationsAreFinished];
1743 // We expect a single record to be uploaded.
1744 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1746 [self addGenericPassword: @"data" account: @"account-delete-me"];
1748 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1749 [self waitForCKModifications];
1751 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1753 // Do not trigger a notification here. This should cause a conflict updating the current key records
1755 // We expect a single record to be uploaded, but that the write will be rejected
1756 // We then expect that item to be reuploaded with the current key
1758 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
1759 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1760 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1762 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under rolled class C key in hierarchy"]];
1764 // New key arrives via SOS!
1765 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1766 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1768 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1771 - (void)testAcceptKeyConflictAndUploadReencryptedItems {
1772 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1773 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1774 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1775 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1777 [self startCKKSSubsystem];
1778 [self.keychainView waitUntilAllOperationsAreFinished];
1780 // We expect a single record to be uploaded.
1781 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1782 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1784 [self addGenericPassword: @"data" account: @"account-delete-me"];
1786 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1787 [self waitForCKModifications];
1789 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1791 // Do not trigger a notification here. This should cause a conflict updating the current key records
1793 // We expect a single record to be uploaded, but that the write will be rejected
1794 // We then expect that item to be reuploaded with the current key
1796 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
1797 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1798 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key-2"];
1799 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1801 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1802 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under rolled class C key in hierarchy"]];
1804 // New key arrives via SOS!
1805 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1806 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1808 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1811 - (void)testAcceptKeyHierarchyResetAndUploadReencryptedItem {
1812 // Test starts with nothing in CloudKit. CKKS uploads a key hierarchy, then it's silently replaced.
1813 // CKKS should notice the replacement, and reupload the item.
1815 [self startCKKSSubsystem];
1817 [self performOctagonTLKUpload:self.ckksViews];
1818 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1820 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
1822 // We expect a single record to be uploaded.
1823 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1825 [self addGenericPassword: @"data" account: @"account-delete-me"];
1827 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1828 [self waitForCKModifications];
1830 // A new peer arrives and resets the world! It sends us a share, though.
1831 CKKSSOSSelfPeer* remotePeer1 = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"remote-peer1"
1832 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1833 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1834 viewList:self.managedViewList];
1835 [self.mockSOSAdapter.trustedPeers addObject:remotePeer1];
1837 NSString* classCUUID = self.keychainZoneKeys.classC.uuid;
1839 self.zones[self.keychainZoneID] = [[FakeCKZone alloc] initZone:self.keychainZoneID];
1840 self.keys[self.keychainZoneID] = nil;
1841 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1842 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:remotePeer1 zoneID:self.keychainZoneID];
1844 XCTAssertNotEqual(classCUUID, self.keychainZoneKeys.classC.uuid, @"Class C UUID should have changed");
1846 // Upon adding an item, we expect a failed OQO, then another OQO with the two items (encrypted correctly)
1847 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1849 [self expectCKModifyItemRecords:2
1850 currentKeyPointerRecords:1
1851 zoneID:self.keychainZoneID
1852 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1854 // We also expect a self share upload, once CKKS figures out the right key hierarchy
1855 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1857 [self addGenericPassword: @"data" account: @"account-delete-me-after-reset"];
1859 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1862 - (void)testRecoverFromRequestKeyRefetchWithoutRolling {
1863 // Simply requesting a key state refetch shouldn't roll the key hierarchy.
1865 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1867 // Spin up CKKS subsystem.
1868 [self startCKKSSubsystem];
1870 // Items should upload.
1871 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1872 [self addGenericPassword: @"data" account: @"account-delete-me"];
1873 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1875 [self waitForCKModifications];
1878 // CKKS should not roll the keys while progressing back to 'ready', but it will fetch once
1879 self.silentFetchesAllowed = false;
1880 [self expectCKFetch];
1882 [self.keychainView dispatchSyncWithAccountKeys: ^bool {
1883 [self.keychainView _onqueueKeyStateMachineRequestFetch];
1887 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
1888 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1891 - (void)testRecoverFromIncrementedCurrentKeyPointerEtag {
1892 // CloudKit sometimes reports the current key pointers have changed (etag mismatch), but their content hasn't.
1893 // In this case, CKKS shouldn't roll the TLK.
1895 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1897 // Spin up CKKS subsystem.
1898 [self startCKKSSubsystem];
1899 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // just to be sure it's fetched
1901 // Items should upload.
1902 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1903 [self addGenericPassword: @"data" account: @"account-delete-me"];
1904 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1906 [self waitForCKModifications];
1908 // Bump the etag on the class C current key record, but don't change any data
1909 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
1910 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
1911 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
1913 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
1914 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
1916 // Add another item. This write should fail, then CKKS should recover without rolling the key hierarchy or issuing a fetch.
1917 self.silentFetchesAllowed = false;
1918 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1919 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1920 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
1921 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1924 - (void)testRecoverMultipleItemsFromIncrementedCurrentKeyPointerEtag {
1925 // CloudKit sometimes reports the current key pointers have changed (etag mismatch), but their content hasn't.
1926 // In this case, CKKS shouldn't roll the TLK.
1927 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1929 // Spin up CKKS subsystem.
1930 [self startCKKSSubsystem];
1931 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // just to be sure it's fetched
1933 // Items should upload.
1934 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1935 [self addGenericPassword: @"data" account: @"account-delete-me"];
1936 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1938 [self waitForCKModifications];
1940 // Bump the etag on the class C current key record, but don't change any data
1941 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
1942 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
1943 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
1945 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
1946 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
1948 // Add another item. This write should fail, then CKKS should recover without rolling the key hierarchy or issuing a fetch.
1949 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
1950 secnotice("ckks", "releasing outgoing-queue hold");
1953 self.silentFetchesAllowed = false;
1954 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1955 [self expectCKModifyItemRecords:2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1956 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
1957 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
1959 [self.operationQueue addOperation: self.keychainView.holdOutgoingQueueOperation];
1960 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1963 - (void)testOnboardOldItemsCreatingKeyHierarchy {
1964 // 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
1966 // Test starts with nothing in CloudKit, and CKKS blocked. Add one item without a UUID...
1968 SecCKKSTestSetDisableAutomaticUUID(true);
1969 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1971 // and an item with a UUID...
1972 SecCKKSTestSetDisableAutomaticUUID(false);
1973 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1975 // We then expect an upload of the added items
1976 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1978 [self startCKKSSubsystem];
1979 [self performOctagonTLKUpload:self.ckksViews];
1981 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1984 - (void)testOnboardOldItemsWithExistingKeyHierarchy {
1985 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1987 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1988 [self addGenericPassword: @"data" account: @"account-delete-me"];
1990 [self startCKKSSubsystem];
1991 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1994 - (void)testOnboardOldItemsWithExistingKeyHierarchyExtantTLK {
1995 // Test starts key hierarchy in our fake CloudKit, the TLK arrived in the local keychain, and CKKS blocked.
1996 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1997 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1998 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2000 // Add one item without a UUID...
2001 SecCKKSTestSetDisableAutomaticUUID(true);
2002 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
2004 // and an item with a UUID...
2005 SecCKKSTestSetDisableAutomaticUUID(false);
2006 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
2008 // 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
2009 // We expect a single record to be uploaded.
2010 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2012 // Spin up CKKS subsystem.
2013 [self startCKKSSubsystem];
2015 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2018 - (void)testOnboardOldItemsWithExistingKeyHierarchyLateTLK {
2019 // Test starts key hierarchy in our fake CloudKit, and CKKS blocked.
2020 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2021 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
2022 self.keychainZone.flag = true;
2024 // Add one item without a UUID...
2025 SecCKKSTestSetDisableAutomaticUUID(true);
2026 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
2028 // and an item with a UUID...
2029 SecCKKSTestSetDisableAutomaticUUID(false);
2030 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
2032 // 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
2034 // Spin up CKKS subsystem.
2035 [self startCKKSSubsystem];
2036 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should have become waitfortlk");
2038 // Now, save the TLK to the keychain (to simulate it coming in via SOS).
2039 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2040 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2042 // We expect a single record to be uploaded.
2043 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2045 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2046 XCTAssertTrue(self.keychainZone.flag, "Keychain zone shouldn't have been reset");
2049 - (void)testResync {
2050 // We need to set up a desynced situation to test our resync.
2051 // First, let CKKS start up and send several items to CloudKit (that we'll then desync!)
2052 __block NSError* error = nil;
2054 // Test starts with keys in CloudKit (so we can create items later)
2055 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2056 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2057 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2059 [self addGenericPassword: @"data" account: @"first"];
2060 [self addGenericPassword: @"data" account: @"second"];
2061 [self addGenericPassword: @"data" account: @"third"];
2062 [self addGenericPassword: @"data" account: @"fourth"];
2063 [self addGenericPassword: @"data" account: @"fifth"];
2064 NSUInteger passwordCount = 5u;
2066 [self checkGenericPassword: @"data" account: @"first"];
2067 [self checkGenericPassword: @"data" account: @"second"];
2068 [self checkGenericPassword: @"data" account: @"third"];
2069 [self checkGenericPassword: @"data" account: @"fourth"];
2070 [self checkGenericPassword: @"data" account: @"fifth"];
2072 [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2074 [self startCKKSSubsystem];
2076 // Wait for uploads to happen
2077 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2078 [self waitForCKModifications];
2079 // One TLK share record
2080 XCTAssertEqual(self.keychainZone.currentDatabase.count, SYSTEM_DB_RECORD_COUNT+passwordCount+1, "Have SYSTEM_DB_RECORD_COUNT+passwordCount+1 objects in cloudkit");
2082 // Now, corrupt away!
2083 // Extract all passwordCount items for Corruption
2084 NSArray<CKRecord*>* items = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
2085 XCTAssertEqual(items.count, passwordCount, "Have %lu Items in cloudkit", (unsigned long)passwordCount);
2087 // For the first record, delete all traces of it from CKKS. But! it remains in local keychain.
2088 // Expected outcome: CKKS resyncs; item exists again.
2089 CKRecord* delete = items[0];
2090 NSString* deleteAccount = [[self decryptRecord: delete] objectForKey: (__bridge id) kSecAttrAccount];
2091 XCTAssertNotNil(deleteAccount, "received an account for the local delete object");
2093 __weak __typeof(self) weakSelf = self;
2094 [self.keychainView dispatchSync:^bool{
2095 __strong __typeof(weakSelf) strongSelf = weakSelf;
2096 XCTAssertNotNil(strongSelf, "self exists");
2098 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2100 [ckme deleteFromDatabase: &error];
2102 XCTAssertNil(error, "no error removing CKME");
2103 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2105 [oqe deleteFromDatabase: &error];
2107 XCTAssertNil(error, "no error removing OQE");
2108 CKKSIncomingQueueEntry* iqe = [CKKSIncomingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2110 [iqe deleteFromDatabase: &error];
2112 XCTAssertNil(error, "no error removing IQE");
2116 // For the second record, delete all traces of it from CloudKit.
2117 // Expected outcome: deleted locally
2118 CKRecord* remoteDelete = items[1];
2119 NSString* remoteDeleteAccount = [[self decryptRecord: remoteDelete] objectForKey: (__bridge id) kSecAttrAccount];
2120 XCTAssertNotNil(remoteDeleteAccount, "received an account for the remote delete object");
2122 [self.keychainZone deleteCKRecordIDFromZone: remoteDelete.recordID];
2123 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
2124 [database removeObjectForKey: remoteDelete.recordID];
2127 // The third record gets modified in CloudKit, but not locally.
2128 // Expected outcome: use the CloudKit version
2129 CKRecord* remoteDataChanged = items[2];
2130 NSMutableDictionary* remoteDataDictionary = [[self decryptRecord: remoteDataChanged] mutableCopy];
2131 NSString* remoteDataChangedAccount = [remoteDataDictionary objectForKey: (__bridge id) kSecAttrAccount];
2132 XCTAssertNotNil(remoteDataChangedAccount, "Received an account for the remote-data-changed object");
2133 remoteDataDictionary[(__bridge id) kSecValueData] = [@"CloudKitWins" dataUsingEncoding: NSUTF8StringEncoding];
2135 CKRecord* newData = [self newRecord: remoteDataChanged.recordID withNewItemData: remoteDataDictionary];
2136 [self.keychainZone addToZone: newData];
2137 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
2138 database[remoteDataChanged.recordID] = newData;
2141 // The fourth record stays in-sync. Good work, everyone!
2142 // Expected outcome: stays in-sync
2143 NSString* insyncAccount = [[self decryptRecord: items[3]] objectForKey: (__bridge id) kSecAttrAccount];
2144 XCTAssertNotNil(insyncAccount, "Received an account for the in-sync object");
2146 // The fifth record is updated locally, but CKKS didn't get the notification, and so the local CKMirror and CloudKit don't have it
2147 // Expected outcome: local change should be steamrolled by the cloud version
2148 CKRecord* localDataChanged = items[4];
2149 NSMutableDictionary* localDataDictionary = [[self decryptRecord: localDataChanged] mutableCopy];
2150 NSString* localDataChangedAccount = [localDataDictionary objectForKey: (__bridge id) kSecAttrAccount];
2152 [self updateGenericPassword:@"newpassword" account:localDataChangedAccount];
2153 [self checkGenericPassword:@"newpassword" account:localDataChangedAccount];
2156 // To make this more challenging, CK returns the refetch in multiple batches. This shouldn't affect the resync...
2157 CKServerChangeToken* ck1 = self.keychainZone.currentChangeToken;
2158 self.silentFetchesAllowed = false;
2159 [self expectCKFetch];
2160 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
2161 // Assert that the fetch is happening with the change token we paused at before
2162 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.keychainZoneID].previousServerChangeToken;
2163 if(changeToken && [changeToken isEqual:ck1]) {
2168 } runBeforeFinished:^{}];
2170 self.keychainZone.limitFetchTo = ck1;
2171 self.keychainZone.limitFetchError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkFailure userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:4]}];
2173 // The sixth record gets magically added to CloudKit, but CKKS has never heard of it
2174 // (emulates a lost record on the client, but that CloudKit already believes it's sent the record for)
2175 // Expected outcome: added to local keychain
2176 NSString* remoteOnlyAccount = @"remote-only";
2177 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount: remoteOnlyAccount];
2178 [self.keychainZone addToZone: ckr];
2179 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
2180 database[ckr.recordID] = ckr;
2183 ckksnotice("ckksresync", self.keychainView, "local delete: %@ %@", delete.recordID.recordName, deleteAccount);
2184 ckksnotice("ckksresync", self.keychainView, "Remote deletion: %@ %@", remoteDelete.recordID.recordName, remoteDeleteAccount);
2185 ckksnotice("ckksresync", self.keychainView, "Remote data changed: %@ %@", remoteDataChanged.recordID.recordName, remoteDataChangedAccount);
2186 ckksnotice("ckksresync", self.keychainView, "in-sync: %@ %@", items[3].recordID.recordName, insyncAccount);
2187 ckksnotice("ckksresync", self.keychainView, "local update: %@ %@", items[4].recordID.recordName, localDataChangedAccount);
2188 ckksnotice("ckksresync", self.keychainView, "Remote only: %@ %@", ckr.recordID.recordName, remoteOnlyAccount);
2190 CKKSSynchronizeOperation* resyncOperation = [self.keychainView resyncWithCloud];
2191 [resyncOperation waitUntilFinished];
2193 XCTAssertNil(resyncOperation.error, "No error during the resync operation");
2195 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2197 // Now do some checking. Remember, we don't know which record we corrupted, so use the parsed account variables to check.
2199 [self findGenericPassword: deleteAccount expecting: errSecSuccess];
2200 [self findGenericPassword: remoteDeleteAccount expecting: errSecItemNotFound];
2201 [self findGenericPassword: remoteDataChangedAccount expecting: errSecSuccess];
2202 [self findGenericPassword: insyncAccount expecting: errSecSuccess];
2203 [self findGenericPassword: localDataChangedAccount expecting: errSecSuccess];
2204 [self findGenericPassword: remoteOnlyAccount expecting: errSecSuccess];
2206 [self checkGenericPassword: @"data" account: deleteAccount];
2207 //[self checkGenericPassword: @"data" account: remoteDeleteAccount];
2208 [self checkGenericPassword: @"CloudKitWins" account: remoteDataChangedAccount];
2209 [self checkGenericPassword: @"data" account: insyncAccount];
2210 [self checkGenericPassword:@"data" account:localDataChangedAccount];
2211 [self checkGenericPassword: @"data" account: remoteOnlyAccount];
2213 [self.keychainView dispatchSync:^bool{
2214 __strong __typeof(weakSelf) strongSelf = weakSelf;
2215 XCTAssertNotNil(strongSelf, "self exists");
2217 CKKSMirrorEntry* ckme = nil;
2219 ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2220 XCTAssertNil(error);
2221 XCTAssertNotNil(ckme);
2223 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDelete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2224 XCTAssertNil(error);
2225 XCTAssertNil(ckme); // deleted!
2227 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDataChanged.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2228 XCTAssertNil(error);
2229 XCTAssertNotNil(ckme);
2231 ckme = [CKKSMirrorEntry tryFromDatabase:items[3].recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2232 XCTAssertNil(error);
2233 XCTAssertNotNil(ckme);
2235 ckme = [CKKSMirrorEntry tryFromDatabase:items[4].recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2236 XCTAssertNil(error);
2237 XCTAssertNotNil(ckme);
2239 ckme = [CKKSMirrorEntry tryFromDatabase:ckr.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2240 XCTAssertNil(error);
2241 XCTAssertNotNil(ckme);
2246 - (void)testResyncItemsMissingFromLocalKeychain {
2247 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2250 // one password correctly synced between local keychain and CloudKit
2251 // one password incorrectly disappeared from local keychain, but in mirror table
2252 // one password sitting in the outgoing queue
2253 // one password sitting in the incoming queue
2255 // Add and sync two passwords
2256 [self addGenericPassword: @"data" account: @"first"];
2257 [self addGenericPassword: @"data" account: @"second"];
2259 [self checkGenericPassword: @"data" account: @"first"];
2260 [self checkGenericPassword: @"data" account: @"second"];
2262 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2263 [self startCKKSSubsystem];
2264 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2265 [self waitForCKModifications];
2266 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2268 // Now, place an item in the outgoing queue
2270 //[self addGenericPassword: @"data" account: @"third"];
2271 //[self checkGenericPassword: @"data" account: @"third"];
2273 // Now, corrupt away!
2274 // Extract all passwordCount items for Corruption
2275 NSArray<CKRecord*>* items = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
2276 XCTAssertEqual(items.count, 2u, "Have %lu Items in cloudkit", (unsigned long)2u);
2278 // For the first record, surreptitiously remove from local keychain
2279 CKRecord* remove = items[0];
2280 NSString* removeAccount = [[self decryptRecord:remove] objectForKey:(__bridge id)kSecAttrAccount];
2281 XCTAssertNotNil(removeAccount, "received an account for the local delete object");
2283 NSURL* kcpath = (__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)@"keychain-2-debug.db");
2285 sqlite3_open([[kcpath path] UTF8String], &db);
2286 NSString* query = [NSString stringWithFormat:@"DELETE FROM genp WHERE uuid=\"%@\"", remove.recordID.recordName];
2287 char* sqlerror = NULL;
2288 XCTAssertEqual(SQLITE_OK, sqlite3_exec(db, [query UTF8String], NULL, NULL, &sqlerror), "SQL deletion shouldn't error");
2289 XCTAssertTrue(sqlerror == NULL, "No error string should have been returned: %s", sqlerror);
2291 sqlite3_free(sqlerror);
2296 // The second record is kept in-sync
2298 // Now, add an in-flight change (for record 3)
2299 [self holdCloudKitModifications];
2300 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2301 [self addGenericPassword:@"data" account:@"third"];
2302 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2304 // For the fourth, add a new record but prevent incoming queue processing
2305 self.keychainView.holdIncomingQueueOperation = [CKKSResultOperation named:@"hold-incoming" withBlock:^{}];
2307 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"fourth"];
2308 [self.keychainZone addToZone:ckr];
2309 [self.keychainView notifyZoneChange:nil];
2311 // Now, where are we....
2312 CKKSScanLocalItemsOperation* scanLocal = [self.keychainView scanLocalItems:@"test-scan"];
2313 [scanLocal waitUntilFinished];
2315 XCTAssertEqual(scanLocal.missingLocalItemsFound, 1u, "Should have found one missing item");
2317 // Allow everything to proceed
2318 [self releaseCloudKitModificationHold];
2319 [self.operationQueue addOperation:self.keychainView.holdIncomingQueueOperation];
2321 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2322 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2324 // And ensure that all four items are present again
2325 [self findGenericPassword: @"first" expecting: errSecSuccess];
2326 [self findGenericPassword: @"second" expecting: errSecSuccess];
2327 [self findGenericPassword: @"third" expecting: errSecSuccess];
2328 [self findGenericPassword: @"fourth" expecting: errSecSuccess];
2331 - (void)testScanItemsChangedInLocalKeychain {
2332 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2334 // Add and sync two passwords
2335 NSString* itemAccount = @"first";
2336 [self addGenericPassword:@"data" account:itemAccount];
2337 [self addGenericPassword:@"data" account:@"second"];
2339 [self checkGenericPassword:@"data" account:itemAccount];
2340 [self checkGenericPassword:@"data" account:@"second"];
2342 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2343 [self startCKKSSubsystem];
2344 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2345 [self waitForCKModifications];
2346 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2348 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
2349 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
2351 // Now, have CKKS miss an update
2353 [self updateGenericPassword:@"newpassword" account:itemAccount];
2354 [self checkGenericPassword:@"newpassword" account:itemAccount];
2357 // Now, where are we....
2358 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
2359 checkItem:[self checkPasswordBlock:self.keychainZoneID account:itemAccount password:@"newpassword"]];
2361 CKKSScanLocalItemsOperation* scanLocal = [self.keychainView scanLocalItems:@"test-scan"];
2362 [scanLocal waitUntilFinished];
2364 XCTAssertEqual(scanLocal.recordsAdded, 1u, "Should have added a single record");
2366 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2368 // And ensure that all four items are present again
2369 [self findGenericPassword: @"first" expecting: errSecSuccess];
2370 [self findGenericPassword: @"second" expecting: errSecSuccess];
2373 - (void)testResyncLocal {
2374 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2375 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2376 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2378 [self addGenericPassword: @"data" account: @"first"];
2379 [self addGenericPassword: @"data" account: @"second"];
2380 NSUInteger passwordCount = 2u;
2382 [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2383 [self startCKKSSubsystem];
2385 // Wait for uploads to happen
2386 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2387 [self waitForCKModifications];
2389 // Local resyncs shouldn't fetch clouds.
2390 self.silentFetchesAllowed = false;
2392 [self deleteGenericPassword:@"first"];
2393 [self deleteGenericPassword:@"second"];
2396 // And they're gone!
2397 [self findGenericPassword:@"first" expecting:errSecItemNotFound];
2398 [self findGenericPassword:@"second" expecting:errSecItemNotFound];
2400 CKKSLocalSynchronizeOperation* op = [self.keychainView resyncLocal];
2401 [op waitUntilFinished];
2402 XCTAssertNil(op.error, "Shouldn't be an error resyncing locally");
2404 // And they're back!
2405 [self checkGenericPassword: @"data" account: @"first"];
2406 [self checkGenericPassword: @"data" account: @"second"];
2409 - (void)testPlistRestoreResyncsLocal {
2410 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2411 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2412 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2414 [self addGenericPassword: @"data" account: @"first"];
2415 [self addGenericPassword: @"data" account: @"second"];
2416 NSUInteger passwordCount = 2u;
2418 [self checkGenericPassword: @"data" account: @"first"];
2419 [self checkGenericPassword: @"data" account: @"second"];
2421 [self expectCKModifyItemRecords:passwordCount currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2422 [self startCKKSSubsystem];
2424 // Wait for uploads to happen
2425 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2426 [self waitForCKModifications];
2429 // This 'restores' a plist keychain backup
2430 // That will kick off a local resync in CKKS, so hold that until we're ready...
2431 self.keychainView.holdLocalSynchronizeOperation = [CKKSResultOperation named:@"hold-local-synchronize" withBlock:^{}];
2433 // Local resyncs shouldn't fetch clouds.
2434 self.silentFetchesAllowed = false;
2436 CFErrorRef cferror = NULL;
2437 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
2438 CFErrorRef cfcferror = NULL;
2440 bool ret = SecServerImportKeychainInPlist(dbt, SecSecurityClientGet(), KEYBAG_NONE, KEYBAG_NONE,
2441 (__bridge CFDictionaryRef)@{}, kSecBackupableItemFilter, false, &cfcferror);
2443 XCTAssertNil(CFBridgingRelease(cfcferror), "Shouldn't error importing a 'backup'");
2444 XCTAssert(ret, "Importing a 'backup' should have succeeded");
2447 XCTAssertNil(CFBridgingRelease(cferror), "Shouldn't error mucking about in the db");
2449 // Restore is additive so original items stick around
2450 [self findGenericPassword:@"first" expecting:errSecSuccess];
2451 [self findGenericPassword:@"second" expecting:errSecSuccess];
2453 // Allow the local resync to continue...
2454 [self.operationQueue addOperation:self.keychainView.holdLocalSynchronizeOperation];
2455 [self.keychainView waitForOperationsOfClass:[CKKSLocalSynchronizeOperation class]];
2457 // Items are still here!
2458 [self checkGenericPassword: @"data" account: @"first"];
2459 [self checkGenericPassword: @"data" account: @"second"];
2462 - (void)testRestartWithoutRefetch {
2463 // Restarting the CKKS operation should check that it's been 15 minutes since the last fetch before it fetches again. Simulate this.
2464 [self startCKKSSubsystem];
2465 [self performOctagonTLKUpload:self.ckksViews];
2467 [self waitForCKModifications];
2468 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2470 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
2472 // Tear down the CKKS object and disallow fetches
2473 [self.keychainView halt];
2474 self.silentFetchesAllowed = false;
2476 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2477 [self beginSOSTrustedViewOperation:self.keychainView];
2478 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
2479 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2481 // Okay, cool, rad, now let's set the date to be very long ago and check that there's positively a fetch
2482 [self.keychainView halt];
2483 self.silentFetchesAllowed = false;
2485 [self.keychainView dispatchSync: ^bool {
2486 NSError* error = nil;
2487 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
2489 XCTAssertNil(error, "no error pulling ckse from database");
2490 XCTAssertNotNil(ckse, "received a ckse");
2492 ckse.lastFetchTime = [NSDate distantPast];
2493 [ckse saveToDatabase: &error];
2494 XCTAssertNil(error, "no error saving to database");
2498 [self expectCKFetch];
2499 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2500 [self beginSOSTrustedViewOperation:self.keychainView];
2501 [self.keychainView waitForKeyHierarchyReadiness];
2502 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2505 - (void)testRecoverFromZoneCreationFailure {
2506 // Fail the zone creation.
2507 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
2508 [self failNextZoneCreation:self.keychainZoneID];
2510 // Spin up CKKS subsystem.
2511 [self startCKKSSubsystem];
2513 // CKKS should figure it out, and fix it
2514 [self performOctagonTLKUpload:self.ckksViews];
2515 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2517 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2518 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2519 [self addGenericPassword: @"data" account: @"account-delete-me"];
2520 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2522 XCTAssertNil(self.zones[self.keychainZoneID].creationError, "Creation error was unset (and so CKKS probably dealt with the error");
2525 - (void)testRecoverFromZoneSubscriptionFailure {
2526 // Fail the zone subscription.
2527 [self failNextZoneSubscription:self.keychainZoneID];
2529 // Spin up CKKS subsystem.
2530 [self startCKKSSubsystem];
2532 // The CKKS subsystem should figure out the issue, and fix it before Octagon uploads its items
2533 [self performOctagonTLKUpload:self.ckksViews];
2535 [self.keychainView waitForKeyHierarchyReadiness];
2536 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2538 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2539 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2540 [self addGenericPassword: @"data" account: @"account-delete-me"];
2541 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2543 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
2546 - (void)testRecoverFromZoneSubscriptionFailureDueToZoneNotExisting {
2547 // This is different from testRecoverFromZoneSubscriptionFailure, since the zone is gone. CKKS must attempt to re-create the zone.
2549 // Silently fail the zone creation
2550 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
2551 [self failNextZoneCreationSilently:self.keychainZoneID];
2553 // Spin up CKKS subsystem.
2554 [self startCKKSSubsystem];
2556 // The CKKS subsystem should figure out the issue, and fix it.
2557 [self performOctagonTLKUpload:self.ckksViews];
2559 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
2560 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2562 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2563 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2564 [self addGenericPassword: @"data" account: @"account-delete-me"];
2565 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2567 XCTAssertFalse(self.zones[self.keychainZoneID].flag, "Zone flag was reset");
2568 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
2571 - (void)testRecoverFromDeletedTLKWithStashedTLK {
2572 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
2574 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2575 NSError* error = nil;
2578 [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
2579 XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
2581 // And delete the non-stashed version
2582 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
2583 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
2585 // Spin up CKKS subsystem.
2586 [self startCKKSSubsystem];
2588 [self.keychainView waitForKeyHierarchyReadiness];
2589 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2591 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2592 [self addGenericPassword: @"data" account: @"account-delete-me"];
2593 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2595 // CKKS should recreate the syncable TLK.
2596 [self checkNSyncableTLKsInKeychain: 1];
2599 - (void)testRecoverFromDeletedTLKWithStashedTLKUponRestart {
2600 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
2602 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2603 // Spin up CKKS subsystem.
2604 [self startCKKSSubsystem];
2605 [self.keychainView waitForKeyHierarchyReadiness];
2607 // Tear down the CKKS object
2608 [self.keychainView halt];
2610 NSError* error = nil;
2613 [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
2614 XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
2616 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
2617 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
2619 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2620 [self beginSOSTrustedViewOperation:self.keychainView];
2621 [self.keychainView waitForKeyHierarchyReadiness];
2622 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2624 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2625 [self addGenericPassword: @"data" account: @"account-delete-me"];
2626 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2628 // CKKS should recreate the syncable TLK.
2629 [self checkNSyncableTLKsInKeychain: 1];
2633 // <rdar://problem/49024967> Octagon: tests for CK exceptions out of cuttlefish
2634 - (void)testRecoverFromTLKWriteFailure {
2635 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
2636 // Test starts with nothing in CloudKit, and will fail the first TLK write.
2637 NSError* noNetwork = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}];
2638 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:noNetwork];
2640 // Spin up CKKS subsystem.
2641 [self startCKKSSubsystem];
2643 // The CKKS subsystem should figure out the issue, and fix it.
2644 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2646 [self.keychainView waitForKeyHierarchyReadiness];
2647 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2649 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2650 [self addGenericPassword: @"data" account: @"account-delete-me"];
2651 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2653 // A network failure creating new TLKs shouldn't delete the 'failed' syncable one.
2654 [self checkNSyncableTLKsInKeychain: 2];
2658 // This test needs to be moved and rewritten now that Octagon handles TLK uploads
2659 // <rdar://problem/49024967> Octagon: tests for CK exceptions out of cuttlefish
2661 - (void)testRecoverFromTLKRace {
2662 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
2663 // Test starts with nothing in CloudKit, and will fail the first TLK write.
2664 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject: ^{
2665 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2668 // Spin up CKKS subsystem.
2669 [self startCKKSSubsystem];
2671 // The first TLK write should fail, and then our fake TLKs should be there in CloudKit.
2672 // It shouldn't write anything back up to CloudKit.
2673 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2675 // Now the TLKs arrive from the other device...
2676 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2677 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2678 [self.keychainView waitForKeyHierarchyReadiness];
2680 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2681 [self addGenericPassword: @"data" account: @"account-delete-me"];
2682 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2684 // A race failure creating new TLKs should delete the old syncable one.
2685 [self checkNSyncableTLKsInKeychain: 1];
2689 - (void)testRecoverFromNullCurrentKeyPointers {
2690 // The current key pointers in cloudkit shouldn't ever not exist if keys do. But, if they don't, CKKS must recover.
2692 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
2693 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2694 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2696 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
2697 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
2698 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2699 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2700 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2702 // Spin up CKKS subsystem.
2703 [self startCKKSSubsystem];
2705 // The CKKS subsystem should figure out the issue, and fix it.
2706 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
2708 [self.keychainView waitForKeyHierarchyReadiness];
2710 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2713 - (void)testRecoverFromNoCurrentKeyPointers {
2714 // The current key pointers in cloudkit shouldn't ever point to nil. But, if they do, CKKS must recover.
2716 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
2717 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2718 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2720 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
2721 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentTLKPointer.storedCKRecord.recordID], "Deleted TLK pointer from zone");
2722 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassAPointer.storedCKRecord.recordID], "Deleted class a pointer from zone");
2723 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassCPointer.storedCKRecord.recordID], "Deleted class c pointer from zone");
2725 // Spin up CKKS subsystem.
2726 [self startCKKSSubsystem];
2728 // The CKKS subsystem should figure out the issue, and fix it.
2729 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
2731 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
2733 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2737 // <rdar://problem/49024967> Octagon: tests for CK exceptions out of cuttlefish
2738 - (void)testRecoverFromBadChangeTag {
2739 // 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.
2741 // Test starts with a broken key hierarchy in our fake CloudKit, but a (incorrectly) up-to-date change tag stored locally.
2742 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2743 SecCKKSTestSetDisableKeyNotifications(true); // Don't tell CKKS about this key material; we're pretending like this is a securityd restart
2744 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2745 SecCKKSTestSetDisableKeyNotifications(false);
2747 [self.keychainView dispatchSync: ^bool {
2748 NSError* error = nil;
2749 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainZoneID.zoneName];
2750 XCTAssertNotNil(ckse, "should have received a ckse");
2752 ckse.ckzonecreated = true;
2753 ckse.ckzonesubscribed = true;
2754 ckse.changeToken = self.keychainZone.currentChangeToken;
2756 [ckse saveToDatabase: &error];
2757 XCTAssertNil(error, "shouldn't have gotten an error saving to database");
2761 // The CKKS subsystem should try to write TLKs, but fail. It'll then upload a TLK share for the keys already in CloudKit
2762 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
2763 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2765 // Spin up CKKS subsystem.
2766 [self startCKKSSubsystem];
2767 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2769 // CKKS should then happily use the keys in CloudKit
2770 [self createClassCItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me"];
2771 [self createClassAItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me-class-a"];
2775 - (void)testRecoverFromDeletedKeysNewItem {
2776 [self startCKKSSubsystem];
2777 [self performOctagonTLKUpload:self.ckksViews];
2779 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
2781 // We expect a single class C record to be uploaded.
2782 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2783 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2785 [self addGenericPassword: @"data" account: @"account-delete-me"];
2786 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2788 [self waitForCKModifications];
2789 [self.keychainView waitUntilAllOperationsAreFinished];
2791 // Now, delete the local keys from the keychain (but leave the synced TLK)
2792 SecCKKSTestSetDisableKeyNotifications(true);
2793 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2794 (id)kSecClass : (id)kSecClassInternetPassword,
2795 (id)kSecUseDataProtectionKeychain : @YES,
2796 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2797 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
2798 }), @"Deleting local keys");
2799 SecCKKSTestSetDisableKeyNotifications(false);
2801 NSError* error = nil;
2802 [self.keychainZoneKeys.classC loadKeyMaterialFromKeychain:&error];
2803 XCTAssertNotNil(error, "Error loading class C key material from keychain");
2805 // We expect a single class C record to be uploaded.
2806 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2807 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2809 [self addGenericPassword: @"datadata" account: @"account-no-keys"];
2810 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2812 // We expect a single class A record to be uploaded.
2813 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2814 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2815 [self addGenericPassword:@"asdf"
2816 account:@"account-class-A"
2818 access:(id)kSecAttrAccessibleWhenUnlocked
2819 expecting:errSecSuccess
2820 message:@"Adding class A item"];
2821 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2824 - (void)testRecoverFromDeletedKeysReceive {
2825 [self startCKKSSubsystem];
2826 [self performOctagonTLKUpload:self.ckksViews];
2828 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
2830 [self waitForCKModifications];
2831 [self.keychainView waitUntilAllOperationsAreFinished];
2833 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2835 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
2837 // Now, delete the local keys from the keychain (but leave the synced TLK)
2838 SecCKKSTestSetDisableKeyNotifications(true);
2839 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2840 (id)kSecClass : (id)kSecClassInternetPassword,
2841 (id)kSecUseDataProtectionKeychain : @YES,
2842 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2843 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
2844 }), @"Deleting local keys");
2845 SecCKKSTestSetDisableKeyNotifications(false);
2847 // Trigger a notification (with hilariously fake data)
2848 [self.keychainZone addToZone: ckr];
2849 [self.keychainView notifyZoneChange:nil];
2850 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2851 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2853 [self findGenericPassword: @"account0" expecting:errSecSuccess];
2856 - (void)testRecoverDeletedTLK {
2857 // If the TLK disappears halfway through, well, that's no good. But we should recover using TLK sharing
2859 [self startCKKSSubsystem];
2860 [self performOctagonTLKUpload:self.ckksViews];
2862 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
2864 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2865 [self waitForCKModifications];
2867 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
2868 [self.keychainView waitUntilAllOperationsAreFinished];
2870 // Now, delete the local keys from the keychain
2871 SecCKKSTestSetDisableKeyNotifications(true);
2872 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2873 (id)kSecClass : (id)kSecClassInternetPassword,
2874 (id)kSecUseDataProtectionKeychain : @YES,
2875 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2876 (id)kSecAttrSynchronizable : (id)kSecAttrSynchronizableAny,
2877 }), @"Deleting CKKS keys");
2878 SecCKKSTestSetDisableKeyNotifications(false);
2880 // Trigger a notification (with hilariously fake data)
2881 [self.keychainZone addToZone: ckr];
2882 [self.keychainView notifyZoneChange:nil];
2884 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2886 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should return to 'ready'");
2888 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // Do this again, to allow for non-atomic key state machinery switching
2890 [self findGenericPassword: @"account0" expecting:errSecSuccess];
2893 - (void)testRecoverMissingRolledKey {
2894 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2896 NSString* accountShouldExist = @"under-rolled-key";
2897 NSString* accountWillExist = @"under-rolled-key-later";
2898 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountShouldExist];
2899 [self.keychainZone addToZone: ckr];
2901 CKRecord* ckrAddedLater = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountWillExist];
2902 CKKSKey* pastClassCKey = self.keychainZoneKeys.classC;
2904 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2905 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2907 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2909 [self startCKKSSubsystem];
2910 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
2912 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2913 [self waitForCKModifications];
2915 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2916 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2917 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
2919 // Now, find and delete the class C key that ckrAddedLater is under
2920 NSError* error = nil;
2921 XCTAssertTrue([pastClassCKey deleteKeyMaterialFromKeychain:&error], "Should be able to delete old key material from keychain");
2922 XCTAssertNil(error, "Should be no error deleting old key material from keychain");
2924 [self.keychainZone addToZone:ckrAddedLater];
2925 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2927 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2928 [self findGenericPassword:accountWillExist expecting:errSecSuccess];
2930 XCTAssertTrue([pastClassCKey loadKeyMaterialFromKeychain:&error], "Class C key should be back in the keychain");
2931 XCTAssertNil(error, "Should be no error loading key from keychain");
2934 - (void)testRecoverMissingRolledClassAKeyWhileLocked {
2935 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2937 NSString* accountShouldExist = @"under-rolled-key";
2938 NSString* accountWillExist = @"under-rolled-key-later";
2939 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountShouldExist key:self.keychainZoneKeys.classA];
2940 [self.keychainZone addToZone: ckr];
2942 CKRecord* ckrAddedLater = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountWillExist key:self.keychainZoneKeys.classA];
2943 CKKSKey* pastClassAKey = self.keychainZoneKeys.classA;
2945 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2946 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2948 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2950 [self startCKKSSubsystem];
2951 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
2953 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2954 [self waitForCKModifications];
2956 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2957 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2958 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
2960 // Now, find and delete the class C key that ckrAddedLater is under
2961 NSError* error = nil;
2962 XCTAssertTrue([pastClassAKey deleteKeyMaterialFromKeychain:&error], "Should be able to delete old key material from keychain");
2963 XCTAssertNil(error, "Should be no error deleting old key material from keychain");
2965 // now, lock the keychain
2966 self.aksLockState = true;
2967 [self.lockStateTracker recheck];
2969 [self.keychainZone addToZone:ckrAddedLater];
2970 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2972 // Item should still not exist due to the lock state....
2973 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2974 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
2976 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], "Key state should have returned to readypendingunlock");
2978 self.aksLockState = false;
2979 [self.lockStateTracker recheck];
2982 [self.keychainView waitUntilAllOperationsAreFinished];
2983 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2984 [self findGenericPassword:accountWillExist expecting:errSecSuccess];
2986 XCTAssertTrue([pastClassAKey loadKeyMaterialFromKeychain:&error], "Class A key should be back in the keychain");
2987 XCTAssertNil(error, "Should be no error loading key from keychain");
2990 - (void)testRecoverFromBadCurrentKeyPointer {
2991 // The current key pointers in cloudkit shouldn't ever point to missing entries. But, if they do, CKKS must recover.
2993 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
2994 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2995 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2997 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
2998 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
2999 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real tlk" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
3000 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class a key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
3001 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class c key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
3003 // Spin up CKKS subsystem.
3004 [self startCKKSSubsystem];
3006 // The CKKS subsystem should figure out the issue, and fix it (while uploading itself a TLK Share)
3007 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3009 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
3011 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3014 - (void)testRecoverFromIncorrectCurrentTLKPointer {
3015 // The current key pointers in cloudkit shouldn't ever point to wrong entries. But, if they do, CKKS must recover.
3017 // Test starts with a rolled hierarchy, and CKPs pointing to the wrong items
3018 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3019 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3021 CKKSCurrentKeyPointer* oldTLKCKP = self.keychainZoneKeys.currentTLKPointer;
3022 CKRecord* oldTLKPointer = [self.keychainZone.currentDatabase[self.keychainZoneKeys.currentTLKPointer.storedCKRecord.recordID] copy];
3024 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3025 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3027 ZoneKeys* newZoneKeys = [self.keychainZoneKeys copy];
3029 // And put the oldTLKPointer back
3030 [self.zones[self.keychainZoneID] addToZone:oldTLKPointer];
3031 self.keychainZoneKeys.currentTLKPointer = oldTLKCKP;
3033 // Make sure it stuck:
3034 XCTAssertNotEqualObjects(self.keychainZoneKeys.currentTLKPointer,
3035 newZoneKeys.currentTLKPointer,
3036 "current TLK pointer should now not point to proper TLK");
3038 // Spin up CKKS subsystem.
3039 [self startCKKSSubsystem];
3041 // The CKKS subsystem should figure out the issue, and fix it (while uploading itself a TLK Share)
3042 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
3044 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
3046 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3047 [self waitForCKModifications];
3049 XCTAssertEqualObjects(self.keychainZoneKeys.currentTLKPointer,
3050 newZoneKeys.currentTLKPointer,
3051 "current TLK pointer should now point to proper TLK");
3052 XCTAssertEqualObjects(self.keychainZoneKeys.currentClassAPointer,
3053 newZoneKeys.currentClassAPointer,
3054 "current Class A pointer should now point to proper Class A key");
3055 XCTAssertEqualObjects(self.keychainZoneKeys.currentClassCPointer,
3056 newZoneKeys.currentClassCPointer,
3057 "current Class C pointer should now point to proper Class C key");
3060 - (void)testRecoverFromDesyncedKeyRecordsViaResync {
3061 // We need to set up a desynced situation to test our resync.
3062 // First, let CKKS start up and send several items to CloudKit (that we'll then desync!)
3063 __block NSError* error = nil;
3065 // Test starts with keys in CloudKit (so we can create items later)
3066 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3067 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3068 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3070 [self addGenericPassword: @"data" account: @"first"];
3071 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
3073 [self startCKKSSubsystem];
3075 // Wait for uploads to happen
3076 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3077 [self waitForCKModifications];
3079 // Now, delete most of the key records are from on-disk, but the change token is not changed
3080 [self.keychainView dispatchSync:^bool{
3081 CKKSCurrentKeySet* keyset = [CKKSCurrentKeySet loadForZone:self.keychainZoneID];
3083 XCTAssertNotNil(keyset.currentTLKPointer, @"should be a TLK pointer");
3084 XCTAssertNotNil(keyset.currentClassAPointer, @"should be a class A pointer");
3085 XCTAssertNotNil(keyset.currentClassCPointer, @"should be a class C pointer");
3087 [keyset.currentTLKPointer deleteFromDatabase:&error];
3088 XCTAssertNil(error, "Should be no error deleting TLK pointer from database");
3089 [keyset.currentClassAPointer deleteFromDatabase:&error];
3090 XCTAssertNil(error, "Should be no error deleting class A pointer from database");
3092 XCTAssertNotNil(keyset.tlk, @"should be a TLK");
3093 XCTAssertNotNil(keyset.classA, @"should be a classA key");
3094 XCTAssertNotNil(keyset.classC, @"should be a classC key");
3096 [keyset.tlk deleteFromDatabase:&error];
3097 XCTAssertNil(error, "Should be no error deleting TLK from database");
3099 [keyset.classA deleteFromDatabase:&error];
3100 XCTAssertNil(error, "Should be no error deleting classA from database");
3102 [keyset.classC deleteFromDatabase:&error];
3103 XCTAssertNil(error, "Should be no error deleting classC from database");
3108 // A restart should realize there's an issue, and pause for help
3109 // 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
3110 [self.keychainView halt];
3111 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
3112 [self.keychainView beginCloudKitOperation];
3113 [self beginSOSTrustedViewOperation:self.keychainView];
3115 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"key state should enter 'waitfortlkcreation'");
3117 // But, a resync should fix you back up
3118 CKKSSynchronizeOperation* resyncOperation = [self.keychainView resyncWithCloud];
3119 [resyncOperation waitUntilFinished];
3120 XCTAssertNil(resyncOperation.error, "No error during the resync operation");
3122 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
3125 - (void)testRecoverFromCloudKitFetchFail {
3126 // Test starts with nothing in database, but one in our fake CloudKit.
3127 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3128 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
3130 // The first two CKRecordZoneChanges should fail with a 'network unavailable' error.
3131 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
3132 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
3134 // Spin up CKKS subsystem.
3135 [self startCKKSSubsystem];
3137 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3138 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3139 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3141 // We expect a single record to be uploaded
3142 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3143 [self addGenericPassword: @"data" account: @"account-delete-me"];
3144 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3146 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3147 [self addGenericPassword:@"asdf"
3148 account:@"account-class-A"
3150 access:(id)kSecAttrAccessibleWhenUnlocked
3151 expecting:errSecSuccess
3152 message:@"Adding class A item"];
3153 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3156 - (void)testRecoverFromCloudKitFetchNetworkFailAfterReady {
3157 // Test starts with nothing in database, but one in our fake CloudKit.
3158 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3160 // Spin up CKKS subsystem.
3161 [self startCKKSSubsystem];
3163 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
3164 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateReady, "CKKS entered ready");
3166 // Network is unavailable
3167 [self.reachabilityTracker setNetworkReachability:false];
3169 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
3170 [self.keychainZone addToZone:ckr];
3172 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3174 // Say network is available
3175 [self.reachabilityTracker setNetworkReachability:true];
3177 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3179 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3182 - (void)testRecoverFromCloudKitFetchNetworkFailBeforeReady {
3183 // Test starts with nothing in database, but one in our fake CloudKit.
3184 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3186 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
3187 [self.keychainZone addToZone:ckr];
3189 // Network is unavailable
3190 [self.reachabilityTracker setNetworkReachability:false];
3192 // Spin up CKKS subsystem.
3193 [self startCKKSSubsystem];
3195 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateInitializing] wait:20*NSEC_PER_SEC], "CKKS entered initializing");
3196 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateInitializing, "CKKS entered initializing");
3198 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3199 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3200 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3202 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3204 // Say network is available
3205 [self.reachabilityTracker setNetworkReachability:true];
3207 [self.keychainView waitUntilAllOperationsAreFinished];
3208 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3210 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3213 - (void)testWaitAfterCloudKitNetworkFailDuringOutgoingQueueOperation {
3214 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3216 [self startCKKSSubsystem];
3218 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered ready");
3220 // Network is now unavailable
3221 [self.reachabilityTracker setNetworkReachability:false];
3223 NSError* noNetwork = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{
3224 CKErrorRetryAfterKey: @(0.2),
3226 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:noNetwork];
3227 [self addGenericPassword: @"data" account: @"account-delete-me"];
3229 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3232 // Once network is available again, the write should happen
3233 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
3234 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3236 [self.reachabilityTracker setNetworkReachability:true];
3238 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3240 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3243 - (void)testRecoverFromCloudKitFetchFailWithDelay {
3244 // Test starts with nothing in database, but one in our fake CloudKit.
3245 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3246 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
3248 // The first CKRecordZoneChanges should fail with a 'delay' error.
3249 self.silentFetchesAllowed = false;
3250 [self.keychainZone failNextFetchWith:[[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorRequestRateLimited userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:4]}]];
3251 [self expectCKFetch];
3253 // Spin up CKKS subsystem.
3254 [self startCKKSSubsystem];
3256 // Ensure it doesn't fetch within these three seconds (if it does, an exception will throw).
3259 // Okay, you can fetch again.
3260 [self expectCKFetch];
3262 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3263 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3264 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3266 // We expect a single record to be uploaded
3267 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3268 [self addGenericPassword: @"data" account: @"account-delete-me"];
3269 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3272 - (void)testHandleZoneDeletedWhileFetching {
3273 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
3274 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3275 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3276 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3278 // 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)
3279 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorZoneNotFound userInfo:@{}]];
3281 [self startCKKSSubsystem];
3283 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"Key state should become 'ready'");
3286 - (void)testRecoverFromCloudKitOldChangeToken {
3287 // Test starts with nothing in database, but one in our fake CloudKit.
3288 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3290 // Spin up CKKS subsystem.
3291 [self startCKKSSubsystem];
3293 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3294 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3295 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3297 // We expect a single record to be uploaded
3298 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3299 [self addGenericPassword: @"data" account: @"account-delete-me"];
3300 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3302 // Delete all old database states, to destroy the change tag validity
3303 [self.keychainZone.pastDatabases removeAllObjects];
3305 // We expect a total local flush and refetch
3306 self.silentFetchesAllowed = false;
3307 [self expectCKFetch]; // one to fail with a CKErrorChangeTokenExpired error
3308 [self expectCKFetch]; // and one to succeed
3310 // Trigger a fake change notification
3311 [self.keychainView notifyZoneChange:nil];
3313 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3315 // And check that a new upload happens just fine.
3316 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3317 [self addGenericPassword:@"asdf"
3318 account:@"account-class-A"
3320 access:(id)kSecAttrAccessibleWhenUnlocked
3321 expecting:errSecSuccess
3322 message:@"Adding class A item"];
3323 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3326 - (void)testRecoverFromCloudKitUnknownDeviceStateRecord {
3327 // Test starts with nothing in database, but one in our fake CloudKit.
3328 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3329 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3330 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3332 // Save a new device state record with some fake etag
3333 [self.keychainView dispatchSync: ^bool {
3334 CKKSDeviceStateEntry* cdse = [[CKKSDeviceStateEntry alloc] initForDevice:self.ckDeviceID
3335 osVersion:@"fake-record"
3336 lastUnlockTime:[NSDate date]
3339 circlePeerID:self.mockSOSAdapter.selfPeer.peerID
3340 circleStatus:kSOSCCInCircle
3341 keyState:SecCKKSZoneKeyStateWaitForTLK
3343 currentClassAUUID:nil
3344 currentClassCUUID:nil
3345 zoneID:self.keychainZoneID
3346 encodedCKRecord:nil];
3347 XCTAssertNotNil(cdse, "Should have created a fake CDSE");
3348 CKRecord* record = [cdse CKRecordWithZoneID:self.keychainZoneID];
3349 XCTAssertNotNil(record, "Should have created a fake CDSE CKRecord");
3350 record.etag = @"fake etag";
3351 cdse.storedCKRecord = record;
3353 NSError* error = nil;
3354 [cdse saveToDatabase:&error];
3355 XCTAssertNil(error, @"No error saving cdse to database");
3360 // Spin up CKKS subsystem.
3361 [self startCKKSSubsystem];
3363 // We expect a record failure, since the device state record is broke
3364 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
3366 // And then we expect a clean write
3367 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
3368 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3370 [self addGenericPassword: @"data" account: @"account-delete-me"];
3371 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3374 - (void)testRecoverFromCloudKitUnknownItemRecord {
3375 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3377 // Spin up CKKS subsystem.
3378 [self startCKKSSubsystem];
3380 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3382 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
3383 [self.keychainZone addToZone:ckr];
3385 [self.keychainView notifyZoneChange:nil];
3386 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3388 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3390 // Delete the record from CloudKit, but miss the notification
3391 XCTAssertNil([self.keychainZone deleteCKRecordIDFromZone: ckr.recordID], "Deleting the record from fake CloudKit should succeed");
3393 // Expect a failed upload when we modify the item
3394 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
3395 [self updateGenericPassword:@"never seen again" account:@"account-delete-me"];
3396 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3398 [self.keychainView waitUntilAllOperationsAreFinished];
3400 // And the item should be disappeared from the local keychain
3401 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3404 - (void)testRecoverFromCloudKitUserDeletedZone {
3405 // Test starts with nothing in database, but one in our fake CloudKit.
3406 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3408 // Spin up CKKS subsystem.
3409 [self startCKKSSubsystem];
3411 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3412 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3413 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3415 // We expect a single record to be uploaded
3416 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3417 [self addGenericPassword: @"data" account: @"account-delete-me"];
3418 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3420 // The first CKRecordZoneChanges should fail with a 'CKErrorUserDeletedZone' error. This will cause a local reset, ending up with zone re-creation.
3421 self.zones[self.keychainZoneID] = nil; // delete the zone
3422 self.keys[self.keychainZoneID] = nil;
3423 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorUserDeletedZone userInfo:@{}]];
3425 // We expect CKKS to recreate the zone, then have octagon reupload the keys, and then the class C item upload
3426 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3428 [self.keychainView notifyZoneChange:nil];
3430 [self performOctagonTLKUpload:self.ckksViews];
3432 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3434 // And check that a new upload occurs.
3435 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3437 [self addGenericPassword:@"asdf"
3438 account:@"account-class-A"
3440 access:(id)kSecAttrAccessibleWhenUnlocked
3441 expecting:errSecSuccess
3442 message:@"Adding class A item"];
3443 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3446 - (void)testRecoverFromCloudKitZoneNotFoundWithoutZoneDeletion {
3447 // Test starts with nothing in database, but one in our fake CloudKit.
3448 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3449 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
3451 // Spin up CKKS subsystem.
3452 [self startCKKSSubsystem];
3454 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3455 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3456 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3458 // We expect a single record to be uploaded
3459 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3460 [self addGenericPassword: @"data" account: @"account-delete-me"];
3461 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3463 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
3465 [self waitForCKModifications];
3466 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
3468 // The next CKRecordZoneChanges will fail with a 'zone not found' error.
3469 self.zones[self.keychainZoneID] = nil; // delete the zone
3470 self.keys[self.keychainZoneID] = nil;
3472 // We expect CKKS to reset itself and recover, then have octagon upload the keys, and then the class C item upload
3473 [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.keychainView notifyZoneChange:nil];
3477 [self performOctagonTLKUpload:self.ckksViews];
3478 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3479 [self waitForCKModifications];
3481 // And check that a new upload occurs.
3482 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3484 [self addGenericPassword:@"asdf"
3485 account:@"account-class-A"
3487 access:(id)kSecAttrAccessibleWhenUnlocked
3488 expecting:errSecSuccess
3489 message:@"Adding class A item"];
3490 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3493 - (void)testRecoverFromCloudKitZoneNotFoundFetchBeforeSigninOccurs {
3494 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
3496 // Before CKKS sign-in, it receives a fetch rpc
3497 XCTestExpectation *fetchReturns = [self expectationWithDescription:@"fetch returned"];
3498 [self.injectedManager rpcFetchAndProcessChanges:nil reply:^(NSError *result) {
3499 XCTAssertNil(result, "Should be no error fetching and processing changes");
3500 [fetchReturns fulfill];
3503 [self startCKKSSubsystem];
3505 [self performOctagonTLKUpload:self.ckksViews];
3506 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
3508 // We expect a single record to be uploaded
3509 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
3510 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3511 [self addGenericPassword: @"data" account: @"account-delete-me"];
3512 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3514 // The fetch should have come back by now
3515 [self waitForExpectations: @[fetchReturns] timeout:5];
3518 - (void)testNoCloudKitAccount {
3519 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
3520 self.accountStatus = CKAccountStatusNoAccount;
3521 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3523 self.silentFetchesAllowed = false;
3524 [self startCKKSSubsystem];
3526 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3528 [self addGenericPassword: @"data" account: @"account-delete-me"];
3529 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3531 // simulate a NSNotification callback (but still logged out)
3532 self.accountStatus = CKAccountStatusNoAccount;
3533 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3535 // There should be no further uploads, even when we save keychain items
3536 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3537 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3539 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3540 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3542 // Test that there are no items in the database (since we never logged in)
3543 [self checkNoCKKSData: self.keychainView];
3546 - (void)testSACloudKitAccount {
3547 // Test starts with nothing in database and the user logged into CloudKit and in circle, but the account is not HSA2.
3548 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3550 self.accountStatus = CKAccountStatusAvailable;
3552 self.silentFetchesAllowed = false;
3554 // Octagon does not initialize the ckks views when not in an HSA2 account
3555 self.automaticallyBeginCKKSViewCloudKitOperation = false;
3556 [self startCKKSSubsystem];
3558 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3560 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3561 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS should enter 'loggedout'");
3563 // There should be no uploads, even when we save keychain items and enter/exit circle
3564 [self addGenericPassword: @"data" account: @"account-delete-me"];
3565 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3567 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3568 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3569 [self endSOSTrustedOperationForAllViews];
3570 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3572 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3573 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3574 [self beginSOSTrustedViewOperation:self.keychainView];
3575 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3577 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3578 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3580 // Test that there are no items in the database (since we never were in an HSA2 account)
3581 [self checkNoCKKSData: self.keychainView];
3584 - (void)testEarlyLogin
3586 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3588 // Octagon should initialize these views
3589 self.automaticallyBeginCKKSViewCloudKitOperation = true;
3591 self.accountStatus = CKAccountStatusAvailable;
3592 //[self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3594 [self startCKKSSubsystem];
3596 // CKKS should end up in 'waitfortlkcreation', as there's no trust and no TLKs
3597 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortlkcreation'");
3599 // Now, renotify the account status, and ensure that CKKS doesn't reenter 'initializing'
3600 CKKSCondition* initializing = self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateInitializing];
3602 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3604 XCTAssertNotEqual(0, [initializing wait:500*NSEC_PER_MSEC], "CKKS should not enter initializing when the device HSA status changes");
3607 - (void)testNoCircle {
3608 // Test starts with nothing in database and the user logged into CloudKit, but out of Circle.
3609 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3611 self.accountStatus = CKAccountStatusAvailable;
3613 [self startCKKSSubsystem];
3615 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3617 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3619 [self addGenericPassword: @"data" account: @"account-delete-me"];
3620 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3622 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortlkcreation'");
3624 // simulate a NSNotification callback (but still logged out)
3625 self.accountStatus = CKAccountStatusNoAccount;
3626 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3628 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'loggedout'");
3630 // There should be no further uploads, even when we save keychain items
3631 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3632 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3634 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3635 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3637 // Test that there are no items in the database (since we never logged in)
3638 [self checkNoCKKSData: self.keychainView];
3641 - (void)testCircleDepartAndRejoin {
3642 // Test starts with CKKS in ready
3643 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3644 [self startCKKSSubsystem];
3646 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
3648 // But then, trust departs
3649 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3650 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3651 [self endSOSTrustedOperationForAllViews];
3653 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortrust'");
3655 // There should be no further uploads, even when we save keychain items
3656 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3657 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3659 // Then trust returns. We expect two uploads
3660 [self expectCKModifyItemRecords:2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
3661 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3662 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3663 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3664 [self beginSOSTrustedViewOperation:self.keychainView];
3666 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3667 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3670 - (void)testCloudKitLogin {
3671 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
3672 self.accountStatus = CKAccountStatusNoAccount;
3673 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3675 // Before we inform CKKS of its account state....
3676 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
3678 [self startCKKSSubsystem];
3680 XCTAssertEqual(0, [self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], "Should have been told of a 'logout' event on startup");
3681 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event shouldn't have happened");
3682 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3684 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3686 // simulate a cloudkit login and NSNotification callback
3687 self.accountStatus = CKAccountStatusAvailable;
3688 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3690 // No writes yet, since we're not in circle
3691 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortlkcreation'");
3692 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3694 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3695 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3696 [self beginSOSTrustedOperationForAllViews];
3698 [self performOctagonTLKUpload:self.ckksViews];
3700 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3701 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3702 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3704 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3705 [self waitForCKModifications];
3707 // We expect a single class C record to be uploaded.
3708 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3709 [self addGenericPassword: @"data" account: @"account-delete-me"];
3711 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3712 [self waitForCKModifications];
3715 - (void)testCloudKitLogoutLogin {
3716 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
3717 [self startCKKSSubsystem];
3718 [self performOctagonTLKUpload:self.ckksViews];
3719 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3720 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3721 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3723 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3724 [self waitForCKModifications];
3726 // We expect a single class C record to be uploaded.
3727 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3728 [self addGenericPassword: @"data" account: @"account-delete-me"];
3730 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3731 [self waitForCKModifications];
3733 // simulate a cloudkit logout and NSNotification callback
3734 self.accountStatus = CKAccountStatusNoAccount;
3735 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3736 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3737 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3738 [self endSOSTrustedOperationForAllViews];
3740 // Test that there are no items in the database after logout
3741 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
3742 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
3743 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3744 [self checkNoCKKSData: self.keychainView];
3746 // There should be no further uploads, even when we save keychain items
3747 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3748 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3750 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3751 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3752 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
3754 // simulate a cloudkit login
3755 // 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
3756 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
3758 self.accountStatus = CKAccountStatusAvailable;
3759 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3761 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3762 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3763 [self beginSOSTrustedViewOperation:self.keychainView];
3765 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3766 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3767 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3769 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3771 // Let everything settle...
3772 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3773 [self waitForCKModifications];
3776 self.accountStatus = CKAccountStatusNoAccount;
3777 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3779 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3780 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3781 [self endSOSTrustedOperationForAllViews];
3783 // Test that there are no items in the database after logout
3784 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
3785 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
3786 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3787 [self checkNoCKKSData: self.keychainView];
3789 // There should be no further uploads, even when we save keychain items
3790 [self addGenericPassword: @"data" account: @"account-delete-me-5"];
3791 [self addGenericPassword: @"data" account: @"account-delete-me-6"];
3793 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3794 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3796 // simulate a cloudkit login
3797 // 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
3798 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
3800 self.accountStatus = CKAccountStatusAvailable;
3801 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3803 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3804 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3805 [self beginSOSTrustedViewOperation:self.keychainView];
3807 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3808 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3809 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3811 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3813 // Let everything settle...
3814 [self.keychainView waitUntilAllOperationsAreFinished];
3815 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3818 self.accountStatus = CKAccountStatusNoAccount;
3819 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3821 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3822 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3823 [self endSOSTrustedOperationForAllViews];
3825 // Test that there are no items in the database after logout
3826 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
3827 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
3828 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3829 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
3830 [self checkNoCKKSData: self.keychainView];
3832 // Force zone into error state
3833 self.keychainView.keyHierarchyState = SecCKKSZoneKeyStateError;
3835 self.accountStatus = CKAccountStatusAvailable;
3836 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3838 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3839 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3840 [self beginSOSTrustedViewOperation:self.keychainView];
3842 XCTestExpectation *operationRun = [self expectationWithDescription:@"operation run"];
3843 NSOperation* op = [NSBlockOperation named:@"test" withBlock:^{
3844 [operationRun fulfill];
3847 [op addDependency:self.keychainView.keyStateReadyDependency];
3848 [self.operationQueue addOperation:op];
3850 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3851 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3852 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3854 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3855 [self waitForExpectations: @[operationRun] timeout:10];
3856 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3859 - (void)testCloudKitLogoutDueToGreyMode {
3860 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
3861 [self startCKKSSubsystem];
3862 [self performOctagonTLKUpload:self.ckksViews];
3863 XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "Should have been told of a 'login'");
3864 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:50*NSEC_PER_MSEC], "'logout' event should be reset");
3865 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3867 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
3869 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3870 [self waitForCKModifications];
3872 // simulate a cloudkit grey mode switch and NSNotification callback. CKKS should treat this as a logout
3873 self.iCloudHasValidCredentials = false;
3874 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3876 // Test that there are no items in the database after logout
3877 XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "Should have been told of a 'logout'");
3878 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:50*NSEC_PER_MSEC], "'login' event should be reset");
3879 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3880 [self checkNoCKKSData: self.keychainView];
3881 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
3883 // There should be no further uploads, even when we save keychain items
3884 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3885 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3887 [self.keychainView waitUntilAllOperationsAreFinished];
3888 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3890 // Also, fetches shouldn't occur
3891 self.silentFetchesAllowed = false;
3892 NSOperation* op = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
3893 CKKSResultOperation* timeoutOp = [CKKSResultOperation named:@"timeout" withBlock:^{}];
3894 [timeoutOp addDependency:op];
3895 [timeoutOp timeout:4*NSEC_PER_SEC];
3896 [self.operationQueue addOperation:timeoutOp];
3897 [timeoutOp waitUntilFinished];
3899 // CloudKit figures its life out. We expect the two passwords from before to be uploaded
3900 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
3901 self.silentFetchesAllowed = true;
3902 self.iCloudHasValidCredentials = true;
3903 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3905 XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "Should have been told of a 'login'");
3906 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:50*NSEC_PER_MSEC], "'logout' event should be reset");
3907 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3908 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3910 // And fetching still works!
3911 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D00" withAccount:@"account0"]];
3912 [self.keychainView notifyZoneChange:nil];
3913 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3914 [self findGenericPassword: @"account0" expecting:errSecSuccess];
3915 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3918 - (void)testCloudKitLoginRace {
3919 // Test starts with nothing in database, and 'in circle', but securityd hasn't received notification if we're logged into CloudKit.
3920 // CKKS should call handleLogout, as the CK account is not present.
3922 // note: don't unblock the ck account state object yet...
3924 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3925 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3927 // Add a keychain item, and make sure it doesn't upload yet.
3928 [self addGenericPassword: @"data" account: @"account-delete-me"];
3929 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3930 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'loggedout'");
3932 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3934 // Now that we're here (and logged out), bring the account up
3936 // We expect a single class C record to be uploaded.
3937 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3939 self.accountStatus = CKAccountStatusAvailable;
3940 [self startCKKSSubsystem];
3941 [self performOctagonTLKUpload:self.ckksViews];
3943 // simulate another NSNotification callback
3944 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3946 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3947 [self waitForCKModifications];
3949 // Make sure new items upload too
3950 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3951 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3952 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3954 [self.keychainView waitUntilAllOperationsAreFinished];
3955 [self waitForCKModifications];
3956 [self.keychainView halt];
3959 - (void)testDontLogOutIfBeforeFirstUnlock {
3961 // test starts as if a previously logged-in device has just rebooted
3962 self.aksLockState = true;
3963 self.accountStatus = CKAccountStatusAvailable;
3965 // This is the original state of the account tracker
3966 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCError error:nil];
3967 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3969 // And this is what the first circle status fetch will actually return
3970 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"]];
3971 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3973 XCTAssertEqual(self.accountStateTracker.currentComputedAccountStatus, CKKSAccountStatusUnknown, "Account tracker status should just be 'unknown'");
3974 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKKS should not yet know the CK account state");
3976 [self startCKKSSubsystem];
3978 XCTAssertEqual(0, [self.keychainView.loggedIn wait:8*NSEC_PER_SEC], "'login' event should have happened");
3979 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:10*NSEC_PER_MSEC], "Should not have been told of a CK 'logout' event on startup");
3980 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:1*NSEC_PER_SEC], "CKKS should know the account state");
3982 // And assume another CK status change
3983 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3984 XCTAssertEqual(self.accountStateTracker.currentComputedAccountStatus, CKKSAccountStatusUnknown, "Account tracker status should just be 'no account'");
3985 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKKS should know the CK account state");
3987 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3989 self.aksLockState = false;
3991 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3992 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3993 [self beginSOSTrustedViewOperation:self.keychainView];
3995 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3996 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3997 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3999 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4000 [self waitForCKModifications];
4002 // We expect a single class C record to be uploaded.
4003 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4004 [self addGenericPassword: @"data" account: @"account-delete-me"];
4006 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4007 [self waitForCKModifications];*/
4010 - (void)testSyncableItemsAddedWhileLoggedOut {
4011 // Test that once CKKS is up and 'logged out', nothing happens when syncable items are added
4012 self.accountStatus = CKAccountStatusNoAccount;
4013 [self startCKKSSubsystem];
4015 XCTAssertEqual([self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], 0, "CKKS should be told that it's logged out");
4017 // CKKS shouldn't decide to poke its state machine, but it should still send the notification
4018 XCTestExpectation* viewChangeNotification = [self expectChangeForView:self.keychainZoneID.zoneName];
4020 // Reject all attempts to trigger a state machine update
4021 id pokeKeyStateMachineScheduler = OCMClassMock([CKKSNearFutureScheduler class]);
4022 OCMReject([pokeKeyStateMachineScheduler trigger]);
4023 self.keychainView.pokeKeyStateMachineScheduler = pokeKeyStateMachineScheduler;
4025 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4027 [self waitForExpectations:@[viewChangeNotification] timeout:8];
4028 [pokeKeyStateMachineScheduler stopMocking];
4031 - (void)testUploadSyncableItemsAddedWhileUntrusted {
4032 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4033 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4035 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
4037 [self startCKKSSubsystem];
4039 XCTAssertEqual([self.keychainView.loggedIn wait:500*NSEC_PER_MSEC], 0, "CKKS should be told that it's logged in");
4041 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered waitfortrust");
4042 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4044 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4048 NSError* error = nil;
4049 NSDictionary* currentOQEs = [CKKSOutgoingQueueEntry countsByStateInZone:self.keychainZoneID error:&error];
4050 XCTAssertNil(error, "Should be no error counting OQEs");
4051 XCTAssertEqual(0, currentOQEs.count, "Should be no OQEs");
4053 // Now, insert a restart to simulate securityd restarting (and throwing away all pending operations), then a real sign in
4054 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4055 [self endSOSTrustedViewOperation:self.keychainView];
4056 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered waitfortrust");
4058 // Okay! Upon sign in, this item should be uploaded
4059 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4060 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4062 [self putSelfTLKSharesInCloudKit:self.keychainZoneID];
4063 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4064 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4065 [self beginSOSTrustedViewOperation:self.keychainView];
4067 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4068 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4071 - (void)testSyncableItemAddedOnDaemonRestartBeforePolicyLoaded {
4072 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
4073 [self startCKKSSubsystem];
4075 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4077 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
4078 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
4081 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4082 [self.injectedManager resetSyncingPolicy];
4083 [self.injectedManager haltZone:self.keychainZoneID.zoneName];
4085 // This item addition shouldn't be uploaded yet, or in any queues
4086 [self addGenericPassword:@"data" account:@"account-delete-me-2"];
4088 NSError* error = nil;
4089 NSDictionary* currentOQEs = [CKKSOutgoingQueueEntry countsByStateInZone:self.keychainZoneID error:&error];
4090 XCTAssertNil(error, "Should be no error counting OQEs");
4091 XCTAssertEqual(0, currentOQEs.count, "Should be no OQEs");
4093 [self.injectedManager setSyncingViews:self.managedViewList sortingPolicy:self.viewSortingPolicyForManagedViewList];
4094 self.keychainView = [self.injectedManager findView:self.keychainZoneID.zoneName];
4095 // end of daemon restart
4097 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4098 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4100 [self.injectedManager beginCloudKitOperationOfAllViews];
4101 [self beginSOSTrustedViewOperation:self.keychainView];
4103 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4104 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4107 // Note that this test assumes that the keychainView object was created at daemon restart.
4108 // I don't really know how to write a test for that...
4109 - (void)testSyncableItemAddedOnDaemonRestartBeforeCloudKitAccountKnown {
4110 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
4111 [self startCKKSSubsystem];
4113 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4116 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4117 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4118 [self beginSOSTrustedViewOperation:self.keychainView];
4120 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4121 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:100*NSEC_PER_MSEC], "CKKS should still have no idea what the account state is");
4122 XCTAssertEqual(self.keychainView.accountStatus, CKKSAccountStatusUnknown, "Account status should be unknown");
4123 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
4125 [self.keychainView beginCloudKitOperation];
4127 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4128 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4129 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4130 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4133 - (void)testSyncableItemModifiedOnDaemonRestartBeforeCloudKitAccountKnown {
4134 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
4135 [self startCKKSSubsystem];
4137 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4139 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4140 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4141 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4142 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4145 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4146 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4147 [self beginSOSTrustedViewOperation:self.keychainView];
4149 [self updateGenericPassword:@"newdata" account: @"account-delete-me-2"];
4150 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:100*NSEC_PER_MSEC], "CKKS should still have no idea what the account state is");
4151 XCTAssertEqual(self.keychainView.accountStatus, CKKSAccountStatusUnknown, "Account status should be unknown");
4152 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
4154 [self.keychainView beginCloudKitOperation];
4156 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4157 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4158 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4159 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4162 - (void)testSyncableItemDeletedOnDaemonRestartBeforeCloudKitAccountKnown {
4163 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
4164 [self startCKKSSubsystem];
4166 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4168 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4169 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4170 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4171 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4174 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4175 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4176 [self beginSOSTrustedViewOperation:self.keychainView];
4178 [self deleteGenericPassword:@"account-delete-me-2"];
4179 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:100*NSEC_PER_MSEC], "CKKS should still have no idea what the account state is");
4180 XCTAssertEqual(self.keychainView.accountStatus, CKKSAccountStatusUnknown, "Account status should be unknown");
4181 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
4183 [self.keychainView beginCloudKitOperation];
4185 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
4186 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4187 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4190 - (void)testNotStuckAfterReset {
4191 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
4193 XCTestExpectation *operationRun = [self expectationWithDescription:@"operation run"];
4194 NSOperation* op = [NSBlockOperation named:@"test" withBlock:^{
4195 [operationRun fulfill];
4198 [op addDependency:self.keychainView.keyStateReadyDependency];
4199 [self.operationQueue addOperation:op];
4201 // And handle a spurious logout
4202 [self.keychainView handleCKLogout];
4204 [self startCKKSSubsystem];
4206 [self waitForExpectations: @[operationRun] timeout:20];
4209 - (void)testCKKSControlBringup {
4210 NSXPCInterface *interface = CKKSSetupControlProtocol([NSXPCInterface interfaceWithProtocol:@protocol(CKKSControlProtocol)]);
4211 XCTAssertNotNil(interface, "Received a configured CKKS interface");
4214 - (void)testMetricsUpload {
4216 XCTestExpectation *upload = [self expectationWithDescription:@"CAMetrics"];
4217 XCTestExpectation *collection = [self expectationWithDescription:@"CAMetrics"];
4219 id saMock = OCMClassMock([SecCoreAnalytics class]);
4220 OCMStub([saMock sendEvent:[OCMArg any] event:[OCMArg any]]).andDo(^(NSInvocation* invocation) {
4224 NSString *sampleSampler = @"stuff";
4226 [[CKKSAnalytics logger] AddMultiSamplerForName:sampleSampler withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSDictionary<NSString *,NSNumber *> *{
4227 [collection fulfill];
4228 return @{ @"hej" : @1 };
4232 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
4233 [self startCKKSSubsystem];
4235 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
4237 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
4238 deletedRecordTypeCounts:nil
4239 zoneID:self.keychainZoneID
4240 checkModifiedRecord:nil
4241 runAfterModification:nil];
4243 [self.injectedManager xpc24HrNotification];
4245 [self waitForExpectations: @[upload, collection] timeout:10];
4246 [[CKKSAnalytics logger] removeMultiSamplerForName:sampleSampler];
4249 - (void)testSaveManyTLKShares {
4250 // Spin up CKKS subsystem.
4251 [self startCKKSSubsystem];
4253 [self performOctagonTLKUpload:self.ckksViews];
4254 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4256 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
4258 NSMutableArray<CKKSSOSSelfPeer*>* peers = [NSMutableArray array];
4260 for(int i = 0; i < 20; i++) {
4261 CKKSSOSSelfPeer* untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:[NSString stringWithFormat:@"untrusted-peer-%d", i]
4262 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
4263 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
4264 viewList:self.managedViewList];
4266 [peers addObject:untrustedPeer];
4269 NSMutableArray<CKRecord*>* tlkShareRecords = [NSMutableArray array];
4271 for(CKKSSOSSelfPeer* peer1 in peers) {
4272 for(CKKSSOSSelfPeer* peer2 in peers) {
4273 NSError* error = nil;
4274 CKKSTLKShareRecord* share = [CKKSTLKShareRecord share:self.keychainZoneKeys.tlk
4280 XCTAssertNil(error, "Should have been no error sharing a CKKSKey");
4281 XCTAssertNotNil(share, "Should be able to create a share");
4283 CKRecord* shareRecord = [share CKRecordWithZoneID:self.keychainZoneID];
4284 [tlkShareRecords addObject:shareRecord];
4288 [self measureBlock:^{
4289 [self.keychainView dispatchSyncWithAccountKeys:^bool{
4290 for(CKRecord* record in tlkShareRecords) {
4291 [self.keychainView _onqueueCKRecordChanged:record resync:false];
4298 - (void)testReceiveNotificationDuringLaunch {
4299 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
4301 [self holdCloudKitModifyRecordZones];
4303 // Spin up CKKS subsystem.
4304 [self startCKKSSubsystem];
4306 CKKSCondition* fetcherCondition = self.keychainView.zoneChangeFetcher.fetchScheduler.liveRequestReceived;
4308 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
4310 [self.keychainView notifyZoneChange:nil];
4312 XCTAssertNotEqual(0, [fetcherCondition wait:(3 * NSEC_PER_SEC)], "not supposed to get a fetch data");
4314 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
4315 self.silentFetchesAllowed = false;
4316 [self expectCKFetch];
4317 [self releaseCloudKitModifyRecordZonesHold];
4319 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4320 OCMVerifyAllWithDelay(self.mockDatabase, 20);