2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
27 #import <CloudKit/CloudKit.h>
28 #import <Foundation/NSXPCConnection_Private.h>
29 #import <XCTest/XCTest.h>
30 #import <OCMock/OCMock.h>
32 #include <Security/SecItemPriv.h>
33 #include "keychain/securityd/SecItemDb.h"
34 #include "keychain/securityd/SecItemServer.h"
35 #include <utilities/SecFileLocations.h>
36 #include "keychain/SecureObjectSync/SOSInternal.h"
38 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
39 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
40 #import "keychain/ckks/CKKS.h"
41 #import "keychain/ckks/CKKSControlProtocol.h"
42 #import "keychain/ckks/CKKSCurrentKeyPointer.h"
43 #import "keychain/ckks/CKKSItemEncrypter.h"
44 #import "keychain/ckks/CKKSKey.h"
45 #import "keychain/ckks/CKKSOutgoingQueueEntry.h"
46 #import "keychain/ckks/CKKSIncomingQueueEntry.h"
47 #import "keychain/ckks/CKKSSynchronizeOperation.h"
48 #import "keychain/ckks/CKKSViewManager.h"
49 #import "keychain/ckks/CKKSZoneStateEntry.h"
50 #import "keychain/ckks/CKKSManifest.h"
51 #import "keychain/ckks/CKKSAnalytics.h"
52 #import "keychain/ckks/CKKSHealKeyHierarchyOperation.h"
53 #import "keychain/ckks/CKKSZoneChangeFetcher.h"
54 #import "keychain/categories/NSError+UsefulConstructors.h"
55 #import "keychain/ckks/CKKSPeer.h"
57 #import "keychain/ckks/tests/MockCloudKit.h"
59 #import "keychain/ckks/tests/CKKSTests.h"
60 #import <utilities/SecCoreAnalytics.h>
63 @interface CKKSLockStateTracker ()
64 @property (nullable) NSDate* lastUnlockedTime;
67 @implementation CloudKitKeychainSyncingTests
71 - (void)testBringupToKeyStateReady {
72 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
73 [self startCKKSSubsystem];
75 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
79 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
81 // We expect a single record to be uploaded.
82 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
84 [self startCKKSSubsystem];
85 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
87 [self addGenericPassword: @"data" account: @"account-delete-me"];
89 OCMVerifyAllWithDelay(self.mockDatabase, 20);
92 - (void)testActiveTLKS {
93 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
95 // We expect a single record to be uploaded.
96 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
98 [self startCKKSSubsystem];
100 [self addGenericPassword: @"data" account: @"account-delete-me"];
102 OCMVerifyAllWithDelay(self.mockDatabase, 20);
104 NSDictionary<NSString *,NSString *>* tlks = [[CKKSViewManager manager] activeTLKs];
106 XCTAssertEqual([tlks count], (NSUInteger)1, "One TLK");
107 XCTAssertNotNil(tlks[@"keychain"], "keychain have a UUID");
111 - (void)testAddMultipleItems {
112 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
113 [self startCKKSSubsystem];
115 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
116 [self addGenericPassword: @"data" account: @"account-delete-me"];
117 OCMVerifyAllWithDelay(self.mockDatabase, 20);
119 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
120 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
121 OCMVerifyAllWithDelay(self.mockDatabase, 20);
123 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
124 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
125 OCMVerifyAllWithDelay(self.mockDatabase, 20);
128 - (void)testAddItemWithoutUUID {
129 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
130 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
131 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
132 [self saveTLKMaterialToKeychain:self.keychainZoneID];
134 [self startCKKSSubsystem];
136 [self.keychainView waitUntilAllOperationsAreFinished];
138 SecCKKSTestSetDisableAutomaticUUID(true);
139 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
141 SecCKKSTestSetDisableAutomaticUUID(false);
143 // We then expect an upload of the added item
144 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
146 OCMVerifyAllWithDelay(self.mockDatabase, 20);
149 - (void)testModifyItem {
150 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
152 NSString* account = @"account-delete-me";
154 [self startCKKSSubsystem];
156 // We expect a single record to be uploaded.
157 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
158 [self addGenericPassword: @"data" account: account];
159 OCMVerifyAllWithDelay(self.mockDatabase, 20);
161 // And then modified.
162 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
163 [self updateGenericPassword: @"otherdata" account:account];
164 OCMVerifyAllWithDelay(self.mockDatabase, 20);
167 - (void)testModifyItemImmediately {
168 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
169 NSString* account = @"account-delete-me";
171 [self startCKKSSubsystem];
172 [self holdCloudKitModifications];
174 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
175 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
176 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
177 [self addGenericPassword: @"data" account: account];
178 OCMVerifyAllWithDelay(self.mockDatabase, 20);
180 // Right now, the write in CloudKit is pending. Make the local modification...
181 [self updateGenericPassword: @"otherdata" account:account];
183 // And then schedule the update
184 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
185 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
186 [self releaseCloudKitModificationHold];
188 OCMVerifyAllWithDelay(self.mockDatabase, 20);
191 - (void)testModifyItemPrimaryKey {
192 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
194 NSString* account = @"account-delete-me";
196 [self startCKKSSubsystem];
198 // We expect a single record to be uploaded.
199 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
200 [self addGenericPassword: @"data" account: account];
201 OCMVerifyAllWithDelay(self.mockDatabase, 20);
203 // And then modified. Since we're changing the "primary key", we expect to delete the old record and upload a new one.
204 [self expectCKModifyItemRecords:1 deletedRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:nil];
205 [self updateAccountOfGenericPassword: @"new-account-delete-me" account:account];
206 OCMVerifyAllWithDelay(self.mockDatabase, 20);
209 - (void)testModifyItemDuringReencrypt {
210 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
211 NSString* account = @"account-delete-me";
213 [self startCKKSSubsystem];
214 [self holdCloudKitModifications];
216 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
217 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
218 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
219 [self addGenericPassword: @"data" account: account];
220 OCMVerifyAllWithDelay(self.mockDatabase, 20);
222 // Right now, the write in CloudKit is pending. Make the local modification...
223 [self updateGenericPassword: @"otherdata" account:account];
225 // And then schedule the update
226 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
227 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
229 // Stop the reencrypt operation from happening
230 self.keychainView.holdReencryptOutgoingItemsOperation = [CKKSGroupOperation named:@"reencrypt-hold" withBlock: ^{
231 secnotice("ckks", "releasing reencryption hold");
234 // The cloudkit operation finishes, letting the next OQO proceed (and set up the reencryption operation)
235 [self releaseCloudKitModificationHold];
237 // And wait for this to finish...
238 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
239 // And once more to quiesce.
240 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
242 // Pause outgoing queue operations to ensure the reencryption operation runs first
243 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
244 secnotice("ckks", "releasing outgoing-queue hold");
247 // Run the reencrypt items operation to completion.
248 [self.operationQueue addOperation: self.keychainView.holdReencryptOutgoingItemsOperation];
249 [self.keychainView waitForOperationsOfClass:[CKKSReencryptOutgoingItemsOperation class]];
251 [self.operationQueue addOperation: self.keychainView.holdOutgoingQueueOperation];
253 OCMVerifyAllWithDelay(self.mockDatabase, 20);
254 [self.keychainView waitUntilAllOperationsAreFinished];
255 [self waitForCKModifications];
258 - (void)testModifyItemBeforeReencrypt {
259 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
260 NSString* account = @"account-delete-me";
262 [self startCKKSSubsystem];
263 [self holdCloudKitModifications];
265 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
266 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
267 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
268 [self addGenericPassword: @"data" account: account];
269 OCMVerifyAllWithDelay(self.mockDatabase, 20);
271 // Right now, the write in CloudKit is pending. Make the local modification...
272 [self updateGenericPassword: @"otherdata" account:account];
274 // And then schedule the update, but for the final version of the password
275 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
276 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"third"]];
278 // Stop the reencrypt operation from happening
279 self.keychainView.holdReencryptOutgoingItemsOperation = [CKKSGroupOperation named:@"reencrypt-hold" withBlock: ^{
280 secnotice("ckks", "releasing reencryption hold");
283 // The cloudkit operation finishes, letting the next OQO proceed (and set up the reencryption operation)
284 [self releaseCloudKitModificationHold];
286 // And wait for this to finish...
287 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
288 // And once more to quiesce.
289 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
291 [self updateGenericPassword: @"third" account:account];
293 // Item should upload.
294 OCMVerifyAllWithDelay(self.mockDatabase, 20);
296 // Run the reencrypt items operation to completion.
297 [self.operationQueue addOperation: self.keychainView.holdReencryptOutgoingItemsOperation];
298 [self.keychainView waitForOperationsOfClass:[CKKSReencryptOutgoingItemsOperation class]];
300 [self.keychainView waitUntilAllOperationsAreFinished];
301 [self waitForCKModifications];
304 - (void)testModifyItemDuringNetworkFailure {
305 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
306 NSString* account = @"account-delete-me";
308 [self startCKKSSubsystem];
309 [self holdCloudKitModifications];
311 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
312 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
314 [self addGenericPassword: @"data" account: account];
315 OCMVerifyAllWithDelay(self.mockDatabase, 20);
317 // Right now, the write in CloudKit is pending. Make the local modification...
318 [self updateGenericPassword: @"otherdata" account:account];
320 // And then schedule the update, but for the final version of the password
321 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
322 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
324 // The cloudkit operation finishes, letting the next OQO proceed (and set up uploading the new item)
325 [self releaseCloudKitModificationHold];
327 // Item should upload.
328 OCMVerifyAllWithDelay(self.mockDatabase, 20);
330 [self.keychainView waitUntilAllOperationsAreFinished];
331 [self waitForCKModifications];
334 - (void)testOutgoingQueueRecoverFromStaleInflightEntry {
335 // CKKS is restarting with an existing in-flight OQE
336 // Note that this test is incomplete, and doesn't re-add the item to the local keychain
337 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
338 NSString* account = @"fake-account";
340 [self.keychainView dispatchSync:^bool {
341 NSError* error = nil;
343 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
345 CKKSItem* item = [self newItem:ckrid withNewItemData:[self fakeRecordDictionary:account zoneID:self.keychainZoneID] key:self.keychainZoneKeys.classC];
346 XCTAssertNotNil(item, "Should be able to create a new fake item");
348 CKKSOutgoingQueueEntry* oqe = [[CKKSOutgoingQueueEntry alloc] initWithCKKSItem:item action:SecCKKSActionAdd state:SecCKKSStateInFlight waitUntil:nil accessGroup:@"ckks"];
349 XCTAssertNotNil(oqe, "Should be able to create a new fake OQE");
350 [oqe saveToDatabase:&error];
352 XCTAssertNil(error, "Shouldn't error saving new OQE to database");
356 NSError *error = NULL;
357 XCTAssertEqual([CKKSOutgoingQueueEntry countByState:SecCKKSStateInFlight zone:self.keychainZoneID error:&error], 1,
358 "Expected on inflight entry in outgoing queue: %@", error);
360 // When CKKS restarts, it should find and re-upload this item
361 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
362 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
364 [self startCKKSSubsystem];
365 [self.keychainView waitForFetchAndIncomingQueueProcessing];
367 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
368 [self beginSOSTrustedViewOperation:self.keychainView];
369 [self.keychainView waitForKeyHierarchyReadiness];
370 OCMVerifyAllWithDelay(self.mockDatabase, 20);
373 - (void)testOutgoingQueueRecoverFromNetworkFailure {
374 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
375 NSString* account = @"account-delete-me";
377 [self startCKKSSubsystem];
378 [self holdCloudKitModifications];
380 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
382 NSError* greyMode = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNotAuthenticated userInfo:@{
383 CKErrorRetryAfterKey: @(0.2),
385 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:greyMode];
387 [self addGenericPassword: @"data" account: account];
388 OCMVerifyAllWithDelay(self.mockDatabase, 20);
390 // And then schedule the retried update
391 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
392 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
394 // The cloudkit operation finishes, letting the next OQO proceed (and set up uploading the new item)
395 [self releaseCloudKitModificationHold];
397 OCMVerifyAllWithDelay(self.mockDatabase, 20);
399 [self.keychainView waitUntilAllOperationsAreFinished];
400 [self waitForCKModifications];
403 - (void)testDeleteItem {
404 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
406 [self startCKKSSubsystem];
408 // We expect a single record to be uploaded.
409 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
410 [self addGenericPassword: @"data" account: @"account-delete-me"];
411 OCMVerifyAllWithDelay(self.mockDatabase, 20);
413 // We expect a single record to be deleted.
414 [self expectCKDeleteItemRecords: 1 zoneID:self.keychainZoneID];
415 [self deleteGenericPassword:@"account-delete-me"];
416 OCMVerifyAllWithDelay(self.mockDatabase, 20);
419 - (void)testDeleteItemImmediatelyAfterModify {
420 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
421 NSString* account = @"account-delete-me";
423 [self startCKKSSubsystem];
425 // We expect a single record to be uploaded.
426 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
427 [self addGenericPassword: @"data" account: account];
428 OCMVerifyAllWithDelay(self.mockDatabase, 20);
430 // Now, hold the modify
431 [self holdCloudKitModifications];
433 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
434 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
435 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
437 [self updateGenericPassword: @"otherdata" account:account];
438 OCMVerifyAllWithDelay(self.mockDatabase, 20);
440 // Right now, the write in CloudKit is pending. Make the local deletion...
441 [self deleteGenericPassword:account];
443 // And then schedule the update
444 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
445 [self releaseCloudKitModificationHold];
447 OCMVerifyAllWithDelay(self.mockDatabase, 20);
450 - (void)testDeleteItemAfterFetchAfterModify {
451 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
452 NSString* account = @"account-delete-me";
454 [self startCKKSSubsystem];
456 // We expect a single record to be uploaded.
457 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
458 [self addGenericPassword: @"data" account: account];
459 OCMVerifyAllWithDelay(self.mockDatabase, 20);
461 // Now, hold the modify
462 //[self holdCloudKitModifications];
464 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
465 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
466 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
468 [self updateGenericPassword: @"otherdata" account:account];
469 OCMVerifyAllWithDelay(self.mockDatabase, 20);
471 // Right now, the write in CloudKit is pending. Place a hold on outgoing queue processing
472 // Place a hold on processing the outgoing queue.
473 CKKSResultOperation* blockOutgoing = [CKKSResultOperation operationWithBlock:^{
474 secnotice("ckks", "Outgoing queue hold released.");
476 blockOutgoing.name = @"outgoing-queue-hold";
477 CKKSResultOperation* outgoingQueueOperation = [self.keychainView processOutgoingQueueAfter:blockOutgoing ckoperationGroup:nil];
479 [self deleteGenericPassword:account];
481 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
483 // Release the CK modification hold
484 //[self releaseCloudKitModificationHold];
487 [self.keychainView waitForFetchAndIncomingQueueProcessing];
488 [self.operationQueue addOperation:blockOutgoing];
489 [outgoingQueueOperation waitUntilFinished];
491 OCMVerifyAllWithDelay(self.mockDatabase, 20);
494 - (void)testDeleteItemWithoutTombstones {
495 // The keychain API allows a client to ask for an inconsistent sync state:
496 // They can ask for a local item deletion without propagating the deletion off-device.
497 // This is the only halfway reasonable way to do keychain item deletions on account signout with the current API
499 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
500 NSString* account = @"account-delete-me";
502 [self startCKKSSubsystem];
503 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"key state should enter 'ready'");
504 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
506 // We expect a single record to be uploaded.
507 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
508 [self addGenericPassword: @"data" account: account];
509 OCMVerifyAllWithDelay(self.mockDatabase, 20);
511 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
513 [self deleteGenericPasswordWithoutTombstones:account];
514 [self findGenericPassword:account expecting:errSecItemNotFound];
516 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
518 // Ensure nothing is in the outgoing queue
519 [self.keychainView dispatchSync:^bool {
520 NSError* error = nil;
521 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID
523 XCTAssertNil(error, "should be no error fetching uuids");
524 XCTAssertEqual(uuids.count, 0u, "There should be zero OQEs");
528 // And a simple fetch doesn't bring it back
529 [self.keychainView waitForFetchAndIncomingQueueProcessing];
530 [self findGenericPassword:account expecting:errSecItemNotFound];
533 CKKSSynchronizeOperation* resyncOperation = [self.keychainView resyncWithCloud];
534 [resyncOperation waitUntilFinished];
535 XCTAssertNil(resyncOperation.error, "No error during the resync operation");
538 [self findGenericPassword:account expecting:errSecSuccess];
540 OCMVerifyAllWithDelay(self.mockDatabase, 20);
544 - (void)testReceiveItem {
545 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
546 [self startCKKSSubsystem];
548 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
549 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
550 (id)kSecAttrAccount : @"account-delete-me",
551 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
552 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
555 CFTypeRef item = NULL;
556 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
558 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
559 [self.keychainZone addToZone: ckr];
561 // Trigger a notification (with hilariously fake data)
562 [self.keychainView notifyZoneChange:nil];
564 [self.keychainView waitForFetchAndIncomingQueueProcessing];
565 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
568 - (void)testReceiveManyItems {
569 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
570 [self startCKKSSubsystem];
572 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D00" withAccount:@"account0"]];
573 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D01" withAccount:@"account1"]];
574 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D02" withAccount:@"account2"]];
575 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D03" withAccount:@"account3"]];
576 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D04" withAccount:@"account4"]];
577 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D05" withAccount:@"account5"]];
578 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D06" withAccount:@"account6"]];
579 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D07" withAccount:@"account7"]];
580 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D08" withAccount:@"account8"]];
581 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D09" withAccount:@"account9"]];
582 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D10" withAccount:@"account10"]];
583 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D11" withAccount:@"account11"]];
585 for(int i = 12; i < 100; i++) {
587 NSString* recordName = [NSString stringWithFormat:@"7B598D31-F9C5-481E-98AC-%012d", i];
588 NSString* account = [NSString stringWithFormat:@"account%d", i];
590 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:recordName withAccount:account]];
594 // Trigger a notification (with hilariously fake data)
595 [self.keychainView notifyZoneChange:nil];
597 [self.keychainView waitForFetchAndIncomingQueueProcessing];
599 [self findGenericPassword: @"account0" expecting:errSecSuccess];
600 [self findGenericPassword: @"account1" expecting:errSecSuccess];
601 [self findGenericPassword: @"account2" expecting:errSecSuccess];
602 [self findGenericPassword: @"account3" expecting:errSecSuccess];
603 [self findGenericPassword: @"account4" expecting:errSecSuccess];
604 [self findGenericPassword: @"account5" expecting:errSecSuccess];
605 [self findGenericPassword: @"account6" expecting:errSecSuccess];
606 [self findGenericPassword: @"account7" expecting:errSecSuccess];
607 [self findGenericPassword: @"account8" expecting:errSecSuccess];
608 [self findGenericPassword: @"account9" expecting:errSecSuccess];
609 [self findGenericPassword: @"account10" expecting:errSecSuccess];
610 [self findGenericPassword: @"account11" expecting:errSecSuccess];
613 - (void)testReceiveCollidingItem {
614 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
615 [self startCKKSSubsystem];
617 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
618 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
619 (id)kSecAttrAccount : @"account-delete-me",
620 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
621 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
624 CFTypeRef item = NULL;
625 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
627 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
628 CKRecord* ckr2 = [self createFakeRecord: self.keychainZoneID recordName: @"F9C58D31-7B59-481E-98AC-5A507ACB2D85"];
630 [self.keychainZone addToZone: ckr];
631 [self.keychainZone addToZone: ckr2];
633 // We expect a delete operation with the "higher" UUID.
634 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
636 // Trigger a notification (with hilariously fake data)
637 [self.keychainView notifyZoneChange:nil];;
639 OCMVerifyAllWithDelay(self.mockDatabase, 20);
640 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
642 [self waitForCKModifications];
643 XCTAssertNil(self.keychainZone.currentDatabase[ckr2.recordID], "Correct record was deleted from CloudKit");
646 -(void)testReceiveItemDelete {
647 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
648 [self startCKKSSubsystem];
650 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
651 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
652 (id)kSecAttrAccount : @"account-delete-me",
653 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
654 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
657 CFTypeRef item = NULL;
658 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
660 [self.keychainView waitForFetchAndIncomingQueueProcessing];
662 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
663 [self.keychainZone addToZone: ckr];
665 // Trigger a notification (with hilariously fake data)
666 [self.keychainView notifyZoneChange:nil];
667 [self.keychainView waitForFetchAndIncomingQueueProcessing];
669 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
673 [self.keychainZone deleteCKRecordIDFromZone: [ckr recordID]];
674 [self.keychainView notifyZoneChange:nil];
675 [self.keychainView waitForFetchAndIncomingQueueProcessing];
677 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
680 -(void)testReceiveItemPhantomDelete {
681 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
682 [self startCKKSSubsystem];
684 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
685 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
686 (id)kSecAttrAccount : @"account-delete-me",
687 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
688 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
691 CFTypeRef item = NULL;
692 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
694 [self.keychainView waitForFetchAndIncomingQueueProcessing];
696 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
697 [self.keychainZone addToZone: ckr];
699 // Trigger a notification (with hilariously fake data)
700 [self.keychainView notifyZoneChange:nil];
701 [self.keychainView waitForFetchAndIncomingQueueProcessing];
703 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
706 [self.keychainView waitUntilAllOperationsAreFinished];
709 [self.keychainZone deleteCKRecordIDFromZone: [ckr recordID]];
711 // and add another, incorrect IQE
712 [self.keychainView dispatchSync: ^bool {
713 // Inefficient, but hey, it works
714 CKRecord* record = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-FFFF-FFFF-5A507ACB2D85"];
715 CKKSItem* fakeItem = [[CKKSItem alloc] initWithCKRecord: record];
717 CKKSIncomingQueueEntry* iqe = [[CKKSIncomingQueueEntry alloc] initWithCKKSItem:fakeItem
718 action:SecCKKSActionDelete
719 state:SecCKKSStateNew];
720 XCTAssertNotNil(iqe, "could create fake IQE");
721 NSError* error = nil;
722 XCTAssert([iqe saveToDatabase: &error], "Saved fake IQE to database");
723 XCTAssertNil(error, "No error saving fake IQE to database");
727 [self.keychainView notifyZoneChange:nil];
728 [self.keychainView waitForFetchAndIncomingQueueProcessing];
730 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
732 // The incoming queue should be empty
733 [self.keychainView dispatchSync: ^bool {
734 NSError* error = nil;
735 NSArray* iqes = [CKKSIncomingQueueEntry all:&error];
736 XCTAssertNil(error, "No error loading IQEs");
737 XCTAssertNotNil(iqes, "Could load IQEs");
738 XCTAssertEqual(iqes.count, 0u, "Incoming queue is empty");
742 -(void)testReceiveConflictOnJustAddedItem {
743 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
744 [self startCKKSSubsystem];
746 [self.keychainView waitForKeyHierarchyReadiness];
747 [self.keychainView waitUntilAllOperationsAreFinished];
749 // Place a hold on processing the outgoing queue.
750 CKKSResultOperation* blockOutgoing = [CKKSResultOperation operationWithBlock:^{
751 secnotice("ckks", "Outgoing queue hold released.");
753 blockOutgoing.name = @"outgoing-queue-hold";
754 CKKSResultOperation* outgoingQueueOperation = [self.keychainView processOutgoingQueueAfter:blockOutgoing ckoperationGroup:nil];
756 CKKSResultOperation* blockIncoming = [CKKSResultOperation operationWithBlock:^{
757 secnotice("ckks", "Incoming queue hold released.");
759 blockIncoming.name = @"incoming-queue-hold";
760 CKKSResultOperation* incomingQueueOperation = [self.keychainView processIncomingQueue:false after: blockIncoming];
762 [self addGenericPassword:@"localchange" account:@"account-delete-me"];
764 // Pull out the new item's UUID.
765 __block NSString* itemUUID = nil;
766 [self.keychainView dispatchSync:^bool {
767 NSError* error = nil;
768 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID ?: [[CKRecordZoneID alloc] initWithZoneName:@"keychain"
769 ownerName:CKCurrentUserDefaultName]
771 XCTAssertNil(error, "no error fetching uuids");
772 XCTAssertEqual(uuids.count, 1u, "There's exactly one outgoing queue entry");
775 XCTAssertNotNil(itemUUID, "Have a UUID for our new item");
779 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName: itemUUID]];
781 [self.keychainView notifyZoneChange:nil];
782 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
784 // Allow the outgoing queue operation to proceed
785 [self.operationQueue addOperation:blockOutgoing];
786 [outgoingQueueOperation waitUntilFinished];
788 // Allow the incoming queue operation to proceed
789 [self.operationQueue addOperation:blockIncoming];
790 [incomingQueueOperation waitUntilFinished];
792 [self checkGenericPassword:@"data" account:@"account-delete-me"];
794 [self.keychainView waitUntilAllOperationsAreFinished];
797 - (void)testReceiveCloudKitConflictOnJustAddedItems {
798 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
799 [self startCKKSSubsystem];
801 [self.keychainView waitForKeyHierarchyReadiness];
802 [self.keychainView waitUntilAllOperationsAreFinished];
804 // Place a hold on processing the outgoing queue.
805 self.keychainView.holdOutgoingQueueOperation = [CKKSResultOperation named:@"outgoing-queue-hold" withBlock:^{
806 secnotice("ckks", "Outgoing queue hold released.");
809 [self addGenericPassword:@"localchange" account:@"account-delete-me"];
811 // Pull out the new item's UUID.
812 __block NSString* itemUUID = nil;
813 [self.keychainView dispatchSync:^bool {
814 NSError* error = nil;
815 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID ?: [[CKRecordZoneID alloc] initWithZoneName:@"keychain"
816 ownerName:CKCurrentUserDefaultName]
818 XCTAssertNil(error, "no error fetching uuids");
819 XCTAssertEqual(uuids.count, 1u, "There's exactly one outgoing queue entry");
822 XCTAssertNotNil(itemUUID, "Have a UUID for our new item");
826 // Add a second item: this item should be uploaded after the failure of the first item
827 [self addGenericPassword:@"localchange" account:@"account-delete-me-2"];
829 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName: itemUUID]];
831 // Also, this write will increment the class C current pointer's etag
832 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
833 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
834 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
835 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
836 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
838 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
839 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
840 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
842 // Allow the outgoing queue operation to proceed
843 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
845 OCMVerifyAllWithDelay(self.mockDatabase, 20);
846 [self.keychainView waitUntilAllOperationsAreFinished];
848 [self checkGenericPassword:@"data" account:@"account-delete-me"];
849 [self checkGenericPassword:@"localchange" account:@"account-delete-me-2"];
853 -(void)testReceiveUnknownField {
854 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
856 [self startCKKSSubsystem];
857 [self.keychainView waitForKeyHierarchyReadiness];
859 NSError* error = nil;
861 // Manually encrypt an item
862 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
863 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
864 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
865 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
866 parentKeyUUID:self.keychainZoneKeys.classA.uuid
867 zoneID:recordID.zoneID];
868 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classA error:&error];
869 XCTAssertNotNil(itemkey, "Got a key");
870 cipheritem.wrappedkey = itemkey.wrappedkey;
871 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
873 NSData* future_data_field = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding];
874 NSString* future_string_field = @"authstring";
875 NSString* future_server_field = @"server_can_change_at_any_time";
876 NSNumber* future_number_field = [NSNumber numberWithInt:30];
878 // Use version 2, so future fields will be authenticated
879 cipheritem.encver = CKKSItemEncryptionVersion2;
880 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
882 authenticatedData[@"future_data_field"] = future_data_field;
883 authenticatedData[@"future_string_field"] = [future_string_field dataUsingEncoding:NSUTF8StringEncoding];
885 uint64_t n = OSSwapHostToLittleConstInt64([future_number_field unsignedLongValue]);
886 authenticatedData[@"future_number_field"] = [NSData dataWithBytes:&n length:sizeof(n)];
889 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
890 XCTAssertNil(error, "no error encrypting object");
891 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
893 CKRecord* ckr = [cipheritem CKRecordWithZoneID: recordID.zoneID];
894 ckr[@"future_data_field"] = future_data_field;
895 ckr[@"future_string_field"] = future_string_field;
896 ckr[@"future_number_field"] = future_number_field;
897 ckr[@"server_new_server_field"] = future_server_field;
898 [self.keychainZone addToZone:ckr];
900 [self.keychainView waitForFetchAndIncomingQueueProcessing];
902 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
903 (id)kSecReturnAttributes: @YES,
904 (id)kSecAttrSynchronizable: @YES,
905 (id)kSecAttrAccount: @"account-delete-me",
906 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
908 CFTypeRef cfresult = NULL;
909 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
911 // Test that if this item is updated, it remains encrypted in v2, and future_field still exists
912 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
913 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
915 OCMVerifyAllWithDelay(self.mockDatabase, 20);
916 [self waitForCKModifications];
918 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
919 XCTAssertEqualObjects(newRecord[@"future_data_field"], future_data_field, "future_data_field still exists");
920 XCTAssertEqualObjects(newRecord[@"future_string_field"], future_string_field, "future_string_field still exists");
921 XCTAssertEqualObjects(newRecord[@"future_number_field"], future_number_field, "future_string_field still exists");
922 XCTAssertEqualObjects(newRecord[@"server_new_server_field"], future_server_field, "future_server_field stille exists");
924 CKKSItem* newItem = [[CKKSItem alloc] initWithCKRecord:newRecord];
925 CKKSAESSIVKey* newItemKey = [self.keychainZoneKeys.classA unwrapAESKey:newItem.wrappedkey error:&error];
926 XCTAssertNil(error, "No error unwrapping AES key");
927 XCTAssertNotNil(newItemKey, "Have an unwrapped AES key for this item");
929 NSDictionary* uploadedData = [CKKSItemEncrypter decryptDictionary:newRecord[SecCKRecordDataKey]
931 authenticatedData:authenticatedData
933 XCTAssertNil(error, "No error decrypting dictionary");
934 XCTAssertNotNil(uploadedData, "Authenticated re-uploaded data including future_field");
935 XCTAssertEqualObjects(uploadedData[@"v_Data"], [@"different password" dataUsingEncoding:NSUTF8StringEncoding], "Passwords match");
939 -(void)testReceiveRecordEncryptedv1 {
940 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
942 [self startCKKSSubsystem];
943 [self.keychainView waitForKeyHierarchyReadiness];
945 NSError* error = nil;
947 // Manually encrypt an item
948 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
949 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
950 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
951 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
952 parentKeyUUID:self.keychainZoneKeys.classC.uuid
953 zoneID:recordID.zoneID];
954 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
955 XCTAssertNotNil(itemkey, "Got a key");
956 cipheritem.wrappedkey = itemkey.wrappedkey;
957 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
959 cipheritem.encver = CKKSItemEncryptionVersion1;
961 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:cipheritem.encver] mutableCopy];
963 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
964 XCTAssertNil(error, "no error encrypting object");
965 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
967 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
969 [self.keychainView waitForFetchAndIncomingQueueProcessing];
971 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
972 (id)kSecReturnAttributes: @YES,
973 (id)kSecAttrSynchronizable: @YES,
974 (id)kSecAttrAccount: @"account-delete-me",
975 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
977 CFTypeRef cfresult = NULL;
978 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
979 CFReleaseNull(cfresult);
981 // Test that if this item is updated, it is encrypted in v2
982 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
983 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
985 OCMVerifyAllWithDelay(self.mockDatabase, 20);
986 [self waitForCKModifications];
988 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
989 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
992 - (void)testUploadPagination {
993 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
995 for(size_t count = 0; count < 250; count++) {
996 [self addGenericPassword: @"data" account: [NSString stringWithFormat:@"account-delete-me-%03lu", count]];
999 [self startCKKSSubsystem];
1001 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1002 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1003 [self expectCKModifyItemRecords: 50 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1005 OCMVerifyAllWithDelay(self.mockDatabase, 40);
1008 - (void)testUploadInitialKeyHierarchy {
1009 // Test starts with nothing in database. CKKS should get into the "please upload my keys" state, then Octagon should perform the upload
1011 // Spin up CKKS subsystem.
1012 [self startCKKSSubsystem];
1014 [self performOctagonTLKUpload:self.ckksViews];
1015 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1017 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
1020 - (void)testDoNotErrorIfNudgedWhileWaitingForTLKUpload {
1021 // Test starts with nothing in database. CKKS should get into the "please upload my keys" state, then Octagon should perform the upload
1023 // Spin up CKKS subsystem.
1024 [self startCKKSSubsystem];
1026 NSMutableArray<CKKSResultOperation<CKKSKeySetProviderOperationProtocol>*>* keysetOps = [NSMutableArray array];
1028 for(CKKSKeychainView* view in self.ckksViews) {
1029 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:40*NSEC_PER_SEC], @"key state should enter 'waitfortlkcreation' (view %@)", view);
1030 [keysetOps addObject: [view findKeySet]];
1033 // Now that we've kicked them all off, wait for them to resolve (and nudge each one, as if a key was saved)
1034 for(CKKSKeychainView* view in self.ckksViews) {
1035 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKUpload] wait:40*NSEC_PER_SEC], @"key state should enter 'waitfortlkupload'");
1037 CKKSCondition* viewProcess = view.keyHierarchyConditions[SecCKKSZoneKeyStateProcess];
1038 [view keyStateMachineRequestProcess];
1039 XCTAssertNotEqual(0, [viewProcess wait:500*NSEC_PER_MSEC], "CKKS should not reprocess the key hierarchy, even if nudged");
1042 // The views should remain in waitfortlkcreation, and not go through process into an error
1044 NSMutableArray<CKRecord*>* keyHierarchyRecords = [NSMutableArray array];
1046 for(CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp in keysetOps) {
1047 // Wait until finished is usually a bad idea. We could rip this out into an operation if we'd like.
1048 [keysetOp waitUntilFinished];
1049 XCTAssertNil(keysetOp.error, "Should be no error fetching keyset from CKKS");
1051 NSArray<CKRecord*>* records = [self putKeySetInCloudKit:keysetOp.keyset];
1052 [keyHierarchyRecords addObjectsFromArray:records];
1055 // Tell our views about our shiny new records!
1056 for(CKKSKeychainView* view in self.ckksViews) {
1057 [view receiveTLKUploadRecords: keyHierarchyRecords];
1059 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1061 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
1064 - (void)testProvideKeysetFromNoTrust {
1066 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1067 [self startCKKSSubsystem];
1069 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlkcreation'");
1070 // I'm not sure how CKKS ends up in 'waitfortrust' without a keyset, so force that state
1071 // In 52301278, it occurred with some complex interaction of zone deletions, fetches, and trust operations
1072 [self.keychainView dispatchSyncWithAccountKeys:^bool{
1073 [self.keychainView _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateWaitForTrust withError:nil];
1076 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortrust'");
1078 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet];
1079 [keysetOp timeout:20*NSEC_PER_SEC];
1080 [keysetOp waitUntilFinished];
1082 XCTAssertNil(keysetOp.error, "Should be no error fetching a keyset");
1085 // This test no longer is very interesting, since Octagon needs to handle lock states, not CKKS...
1086 - (void)testUploadInitialKeyHierarchyAfterLockedStart {
1087 // 'Lock' the keybag
1088 self.aksLockState = true;
1089 [self.lockStateTracker recheck];
1091 [self startCKKSSubsystem];
1093 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1094 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitfortlkcreation");
1096 // After unlock, the key hierarchy should be created.
1097 self.aksLockState = false;
1098 [self.lockStateTracker recheck];
1100 [self performOctagonTLKUpload:self.ckksViews];
1102 // We expect a single class C record to be uploaded.
1103 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1105 [self addGenericPassword: @"data" account: @"account-delete-me"];
1106 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1109 - (void)testLockImmediatelyAfterUploadingInitialKeyHierarchy {
1111 __weak __typeof(self) weakSelf = self;
1113 [self startCKKSSubsystem];
1114 [self performOctagonTLKUpload:self.ckksViews afterUpload:^{
1115 __strong __typeof(self) strongSelf = weakSelf;
1116 [strongSelf holdCloudKitFetches];
1119 // Should enter 'ready'
1120 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1121 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1123 // Now, lock and allow fetches again
1124 self.aksLockState = true;
1125 [self.lockStateTracker recheck];
1126 [self releaseCloudKitFetchHold];
1128 CKKSResultOperation* op = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
1129 [op waitUntilFinished];
1131 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1133 // Wait for CKKS to shake itself out...
1134 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
1136 // Should be in ReadyPendingUnlock
1137 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1139 // We expect a single class C record to be uploaded.
1140 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1141 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1143 [self addGenericPassword: @"data" account: @"account-delete-me"];
1144 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1147 - (void)testReceiveKeyHierarchyAfterLockedStart {
1148 // 'Lock' the keybag
1149 self.aksLockState = true;
1150 [self.lockStateTracker recheck];
1152 [self startCKKSSubsystem];
1154 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1155 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitfortlkcreation");
1157 // Now, another device comes along and creates the hierarchy; we download it; and it and sends us the TLK
1158 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1159 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1160 [self.keychainView notifyZoneChange:nil];
1161 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
1163 self.aksLockState = false;
1164 [self.lockStateTracker recheck];
1165 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should end up in waitfortlk");
1167 // After unlock, the TLK arrives
1168 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1169 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1171 // We expect a single class C record to be uploaded.
1172 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1174 [self addGenericPassword: @"data" account: @"account-delete-me"];
1175 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1178 - (void)testLoadKeyHierarchyAfterLockedStart {
1179 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
1181 // 'Lock' the keybag
1182 self.aksLockState = true;
1183 [self.lockStateTracker recheck];
1185 [self startCKKSSubsystem];
1187 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1188 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1190 self.aksLockState = false;
1191 [self.lockStateTracker recheck];
1193 // We expect a single class C record to be uploaded.
1194 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1196 [self addGenericPassword: @"data" account: @"account-delete-me"];
1197 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1200 - (void)testUploadAndUseKeyHierarchy {
1201 [self startCKKSSubsystem];
1202 [self performOctagonTLKUpload:self.ckksViews];
1204 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
1205 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1206 (id)kSecAttrAccount : @"account-delete-me",
1207 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1208 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1210 CFTypeRef item = NULL;
1211 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not exist");
1213 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1214 [self waitForCKModifications];
1216 // We expect a single class C record to be uploaded.
1217 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1219 [self addGenericPassword: @"data" account: @"account-delete-me"];
1220 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1222 // now, expect a single class A record to be uploaded
1223 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1225 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef)@{
1226 (id)kSecClass : (id)kSecClassGenericPassword,
1227 (id)kSecAttrAccessGroup : @"com.apple.security.sos",
1228 (id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked,
1229 (id)kSecAttrAccount : @"account-class-A",
1230 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1231 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
1232 }, NULL), @"Adding class A item");
1233 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1236 - (void)testUploadInitialKeyHierarchyTriggersBackup {
1237 // We also expect the view manager's notifyNewTLKsInKeychain call to fire (after some delay)
1238 OCMExpect([self.mockCKKSViewManager notifyNewTLKsInKeychain]);
1240 // Spin up CKKS subsystem.
1241 [self startCKKSSubsystem];
1242 [self performOctagonTLKUpload:self.ckksViews];
1244 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1245 OCMVerifyAllWithDelay(self.mockCKKSViewManager, 10);
1248 - (void)testResetCloudKitZoneFromNoTLK {
1249 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1250 OCMExpect([self.suggestTLKUpload trigger]);
1252 self.silentZoneDeletesAllowed = true;
1254 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1255 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1256 // explicitly do not save a fake device status here
1257 self.keychainZone.flag = true;
1259 [self startCKKSSubsystem];
1260 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1262 // But then, it'll fire off the reset and reach 'ready', with a little help from octagon
1263 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1264 [self performOctagonTLKUpload:self.ckksViews];
1266 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1267 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1269 // And the zone should have been cleared and re-made
1270 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1273 - (void)testResetCloudKitZoneFromNoTLKWithOtherWaitForTLKDevices {
1274 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1275 OCMExpect([self.suggestTLKUpload trigger]);
1277 self.silentZoneDeletesAllowed = true;
1279 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1280 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1281 // Save a fake device status here, but modify its key state to be 'waitfortlk': it has no idea what the TLK is either
1282 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1283 [self putFakeOctagonOnlyDeviceStatusInCloudKit:self.keychainZoneID];
1285 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1286 if([record.recordType isEqualToString:SecCKRecordDeviceStateType]) {
1287 record[SecCKRecordKeyState] = CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTLK);
1291 self.keychainZone.flag = true;
1293 [self startCKKSSubsystem];
1294 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1296 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1297 [self performOctagonTLKUpload:self.ckksViews];
1299 // But then, it'll fire off the reset and reach 'ready'
1300 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1301 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1303 // And the zone should have been cleared and re-made
1304 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1307 - (void)testResetCloudKitZoneFromNoTLKIgnoringInactiveDevices {
1308 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1309 OCMExpect([self.suggestTLKUpload trigger]);
1311 self.silentZoneDeletesAllowed = true;
1313 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1314 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1315 // Save a fake device status here, but modify its creation and modification times to be months ago
1316 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1317 [self putFakeOctagonOnlyDeviceStatusInCloudKit:self.keychainZoneID];
1319 // Put a 'in-circle' TLKShare record, but also modify its creation and modification times
1320 CKKSSOSSelfPeer* untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"untrusted-peer"
1321 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1322 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1323 viewList:self.managedViewList];
1324 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:untrustedPeer to:untrustedPeer zoneID:self.keychainZoneID];
1326 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1327 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1328 record.creationDate = [NSDate distantPast];
1329 record.modificationDate = [NSDate distantPast];
1333 self.keychainZone.flag = true;
1335 [self startCKKSSubsystem];
1336 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1338 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1339 [self performOctagonTLKUpload:self.ckksViews];
1341 // But then, it'll fire off the reset and reach 'ready'
1342 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1343 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1345 // And the zone should have been cleared and re-made
1346 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1349 - (void)testDoNotResetCloudKitZoneDuringBadCircleState {
1350 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1351 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1353 // This test has stuff in CloudKit, but no TLKs.
1354 // CKKS should NOT reset the CK zone.
1355 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1356 self.zones[self.keychainZoneID].flag = true;
1358 [self startCKKSSubsystem];
1360 // But since we're out of circle, this test needs to initialize the zone itself
1361 [self.keychainView beginCloudKitOperation];
1363 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered waitfortrust");
1364 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1366 FakeCKZone* keychainZone = self.zones[self.keychainZoneID];
1367 XCTAssertNotNil(keychainZone, "Should still have a keychain zone");
1368 XCTAssertTrue(keychainZone.flag, "keychain zone should not have been recreated");
1371 - (void)testDoNotResetCloudKitZoneFromWaitForTLKDueToRecentDeviceState {
1372 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1374 // CKKS shouldn't reset this zone, due to a recent device status claiming to have TLKs
1375 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1377 // Also, CKKS _should_ be able to return the key hierarchy if asked before it starts
1378 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet];
1380 NSDateComponents* offset = [[NSDateComponents alloc] init];
1382 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1383 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1384 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1385 record.creationDate = updateTime;
1386 record.modificationDate = updateTime;
1390 self.keychainZone.flag = true;
1391 [self startCKKSSubsystem];
1393 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1395 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1397 // And, ensure that the keyset op ran and has results
1398 CKKSResultOperation* waitOp = [CKKSResultOperation named:@"test op" withBlock:^{}];
1399 [waitOp addDependency:keysetOp];
1400 [waitOp timeout:2*NSEC_PER_SEC];
1401 [self.operationQueue addOperation:waitOp];
1402 [waitOp waitUntilFinished];
1404 XCTAssert(keysetOp.finished, "Keyset op should have finished");
1405 XCTAssertNil(keysetOp.error, "keyset op should not have errored");
1406 XCTAssertNotNil(keysetOp.keyset, "keyset op should have a keyset");
1407 XCTAssertNotNil(keysetOp.keyset.currentTLKPointer, "keyset should have a current TLK pointer");
1408 XCTAssertEqualObjects(keysetOp.keyset.currentTLKPointer.currentKeyUUID, self.keychainZoneKeys.tlk.uuid, "keyset should match what's in zone");
1411 - (void)testDoNotCloudKitZoneFromWaitForTLKDueToRecentButUntrustedDeviceState {
1412 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1414 // CKKS should reset this zone, even though to a recent device status claiming to have TLKs. The device isn't trusted
1415 self.silentZoneDeletesAllowed = true;
1416 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1417 [self.mockSOSAdapter.trustedPeers removeObject:self.remoteSOSOnlyPeer];
1419 self.keychainZone.flag = true;
1420 [self startCKKSSubsystem];
1422 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1423 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1425 // And ensure it doesn't go on to 'reset'
1426 XCTAssertNotEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:100*NSEC_PER_MSEC], @"Key state should not become 'resetzone'");
1429 - (void)testResetCloudKitZoneFromWaitForTLKDueToLessRecentAndUntrustedDeviceState {
1430 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1431 OCMExpect([self.suggestTLKUpload trigger]);
1433 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1435 // CKKS should reset this zone, even though to a recent device status claiming to have TLKs. The device isn't trusted
1436 self.silentZoneDeletesAllowed = true;
1437 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1438 [self.mockSOSAdapter.trustedPeers removeObject:self.remoteSOSOnlyPeer];
1440 NSDateComponents* offset = [[NSDateComponents alloc] init];
1442 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1443 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1444 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1445 record.creationDate = updateTime;
1446 record.modificationDate = updateTime;
1450 self.keychainZone.flag = true;
1451 [self startCKKSSubsystem];
1452 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1454 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1455 [self performOctagonTLKUpload:self.ckksViews];
1457 // Then we should reset.
1458 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1459 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1461 // And the zone should have been cleared and re-made
1462 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1465 - (void)testAcceptExistingKeyHierarchy {
1466 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
1467 // Test also begins with the TLK having arrived in the local keychain (via SOS)
1468 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1469 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1470 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1472 // Spin up CKKS subsystem.
1473 [self startCKKSSubsystem];
1475 // The CKKS subsystem should only upload its TLK share
1476 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1478 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1480 // Verify that there are three local keys, and three local current key records
1481 __weak __typeof(self) weakSelf = self;
1482 [self.keychainView dispatchSync: ^bool{
1483 __strong __typeof(weakSelf) strongSelf = weakSelf;
1484 XCTAssertNotNil(strongSelf, "self exists");
1486 NSError* error = nil;
1488 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
1489 XCTAssertNil(error, "no error fetching keys");
1490 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
1492 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1493 XCTAssertNil(error, "no error fetching current keys");
1494 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1500 - (void)testAcceptExistingAndUseKeyHierarchy {
1501 // Test starts with nothing in database, but one in our fake CloudKit.
1502 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1503 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1504 // But, CKKS shouldn't ever reset the zone
1505 self.keychainZone.flag = true;
1507 [self startCKKSSubsystem];
1508 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:200*NSEC_PER_SEC], "Key state should have become waitfortlk");
1510 // Now, save the TLK to the keychain (to simulate it coming in later via SOS). We'll create a TLK share for ourselves.
1511 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1512 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1514 // Wait for the key hierarchy to sort itself out, to make it easier on this test; see testOnboardOldItemsWithExistingKeyHierarchy for the other test.
1515 // The CKKS subsystem should write its TLK share, but nothing else
1516 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1518 // We expect a single record to be uploaded for each key class
1519 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1520 [self addGenericPassword: @"data" account: @"account-delete-me"];
1521 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1523 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1524 [self addGenericPassword:@"asdf"
1525 account:@"account-class-A"
1527 access:(id)kSecAttrAccessibleWhenUnlocked
1528 expecting:errSecSuccess
1529 message:@"Adding class A item"];
1530 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1531 XCTAssertTrue(self.keychainZone.flag, "Keychain zone shouldn't have been reset");
1534 - (void)testAcceptExistingKeyHierarchyDespiteLocked {
1535 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
1536 // Test also begins with the TLK having arrived in the local keychain (via SOS)
1538 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1539 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1541 self.aksLockState = true;
1542 [self.lockStateTracker recheck];
1544 // Spin up CKKS subsystem.
1545 [self startCKKSSubsystem];
1547 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:20*NSEC_PER_SEC], "Key state should have become waitforunlock");
1549 // CKKS will give itself a TLK Share
1550 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1552 // Now that all operations are complete, 'unlock' AKS
1553 self.aksLockState = false;
1554 [self.lockStateTracker recheck];
1556 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1557 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
1559 // Verify that there are three local keys, and three local current key records
1560 __weak __typeof(self) weakSelf = self;
1561 [self.keychainView dispatchSync: ^bool{
1562 __strong __typeof(weakSelf) strongSelf = weakSelf;
1563 XCTAssertNotNil(strongSelf, "self exists");
1565 NSError* error = nil;
1567 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
1568 XCTAssertNil(error, "no error fetching keys");
1569 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
1571 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1572 XCTAssertNil(error, "no error fetching current keys");
1573 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1579 - (void)testReceiveClassCWhileALocked {
1580 // Test starts with a key hierarchy already existing.
1581 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
1582 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1583 [self startCKKSSubsystem];
1585 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1586 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1588 [self findGenericPassword:@"classCItem" expecting:errSecItemNotFound];
1589 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
1591 // 'Lock' the keybag
1592 self.aksLockState = true;
1593 [self.lockStateTracker recheck];
1595 XCTAssertNotNil(self.keychainZoneKeys, "Have zone keys for zone");
1596 XCTAssertNotNil(self.keychainZoneKeys.classA, "Have class A key for zone");
1597 XCTAssertNotNil(self.keychainZoneKeys.classC, "Have class C key for zone");
1599 [self.keychainView dispatchSyncWithAccountKeys: ^bool {
1600 [self.keychainView _onqueueKeyStateMachineRequestProcess];
1603 // And ensure we end up back in 'readypendingunlock': we have the keys, we're just locked now
1604 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
1605 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1607 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"classCItem" key:self.keychainZoneKeys.classC]];
1608 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-FFFF-FFFF-FFFF-5A507ACB2D85" withAccount:@"classAItem" key:self.keychainZoneKeys.classA]];
1610 CKKSResultOperation* op = [self.keychainView waitForFetchAndIncomingQueueProcessing];
1611 // The processing op should NOT error, even though it didn't manage to process the classA item
1612 XCTAssertNil(op.error, "no error while failing to process a class A item");
1614 CKKSResultOperation* erroringOp = [self.keychainView processIncomingQueue:true];
1615 [erroringOp waitUntilFinished];
1616 XCTAssertNotNil(erroringOp.error, "error exists while processing a class A item");
1618 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
1619 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
1621 self.aksLockState = false;
1622 [self.lockStateTracker recheck];
1623 [self.keychainView waitUntilAllOperationsAreFinished];
1625 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
1626 [self findGenericPassword:@"classAItem" expecting:errSecSuccess];
1629 - (void)testRestartWhileLocked {
1630 [self startCKKSSubsystem];
1631 [self performOctagonTLKUpload:self.ckksViews];
1633 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1635 // 'Lock' the keybag
1636 self.aksLockState = true;
1637 [self.lockStateTracker recheck];
1639 [self.keychainView halt];
1640 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1641 [self.keychainView beginCloudKitOperation];
1642 [self beginSOSTrustedViewOperation:self.keychainView];
1644 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1646 self.aksLockState = false;
1647 [self.lockStateTracker recheck];
1649 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1652 - (void)testExternalKeyRoll {
1653 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1654 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1655 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1656 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1658 // Spin up CKKS subsystem.
1659 [self startCKKSSubsystem];
1661 // The CKKS subsystem should not try to write anything to the CloudKit database.
1662 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1664 __weak __typeof(self) weakSelf = self;
1666 // We expect a single record to be uploaded.
1667 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1669 [self addGenericPassword: @"data" account: @"account-delete-me"];
1671 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1672 [self waitForCKModifications];
1674 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1675 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1676 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1678 // Trigger a notification
1679 [self.keychainView notifyZoneChange:nil];
1681 // Make life easy on this test; testAcceptKeyConflictAndUploadReencryptedItem will check the case when we don't receive the notification
1682 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1684 // Just in extra case of threading issues, force a reexamination of the key hierarchy
1685 [self.keychainView dispatchSyncWithAccountKeys: ^bool {
1686 [self.keychainView _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
1690 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1692 // Verify that there are six local keys, and three local current key records
1693 [self.keychainView dispatchSync: ^bool{
1694 __strong __typeof(weakSelf) strongSelf = weakSelf;
1695 XCTAssertNotNil(strongSelf, "self exists");
1697 NSError* error = nil;
1698 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:self.keychainZoneID error:&error];
1699 XCTAssertNil(error, "no error fetching keys");
1700 XCTAssertEqual(keys.count, 6u, "Six keys in local database");
1702 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1703 XCTAssertNil(error, "no error fetching current keys");
1704 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1706 for(CKKSCurrentKeyPointer* key in currentkeys) {
1707 if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
1708 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.tlk.uuid);
1709 } else if([key.keyclass isEqualToString: SecCKKSKeyClassA]) {
1710 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classA.uuid);
1711 } else if([key.keyclass isEqualToString: SecCKKSKeyClassC]) {
1712 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classC.uuid);
1714 XCTFail("Unknown key class: %@", key.keyclass);
1721 // We expect a single record to be uploaded.
1722 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1724 // TODO: remove this by writing code for item reencrypt after key arrival
1725 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1727 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1729 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1732 - (void)testAcceptKeyConflictAndUploadReencryptedItem {
1733 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1734 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1735 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1736 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1738 [self startCKKSSubsystem];
1739 [self.keychainView waitUntilAllOperationsAreFinished];
1741 // We expect a single record to be uploaded.
1742 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1744 [self addGenericPassword: @"data" account: @"account-delete-me"];
1746 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1747 [self waitForCKModifications];
1749 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1751 // Do not trigger a notification here. This should cause a conflict updating the current key records
1753 // We expect a single record to be uploaded, but that the write will be rejected
1754 // We then expect that item to be reuploaded with the current key
1756 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
1757 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1758 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1760 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under rolled class C key in hierarchy"]];
1762 // New key arrives via SOS!
1763 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1764 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1766 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1769 - (void)testAcceptKeyConflictAndUploadReencryptedItems {
1770 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1771 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1772 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1773 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1775 [self startCKKSSubsystem];
1776 [self.keychainView waitUntilAllOperationsAreFinished];
1778 // We expect a single record to be uploaded.
1779 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1780 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1782 [self addGenericPassword: @"data" account: @"account-delete-me"];
1784 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1785 [self waitForCKModifications];
1787 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1789 // Do not trigger a notification here. This should cause a conflict updating the current key records
1791 // We expect a single record to be uploaded, but that the write will be rejected
1792 // We then expect that item to be reuploaded with the current key
1794 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
1795 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1796 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key-2"];
1797 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1799 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1800 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under rolled class C key in hierarchy"]];
1802 // New key arrives via SOS!
1803 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1804 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1806 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1809 - (void)testAcceptKeyHierarchyResetAndUploadReencryptedItem {
1810 // Test starts with nothing in CloudKit. CKKS uploads a key hierarchy, then it's silently replaced.
1811 // CKKS should notice the replacement, and reupload the item.
1813 [self startCKKSSubsystem];
1815 [self performOctagonTLKUpload:self.ckksViews];
1816 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1818 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
1820 // We expect a single record to be uploaded.
1821 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1823 [self addGenericPassword: @"data" account: @"account-delete-me"];
1825 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1826 [self waitForCKModifications];
1828 // A new peer arrives and resets the world! It sends us a share, though.
1829 CKKSSOSSelfPeer* remotePeer1 = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"remote-peer1"
1830 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1831 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1832 viewList:self.managedViewList];
1833 [self.mockSOSAdapter.trustedPeers addObject:remotePeer1];
1835 NSString* classCUUID = self.keychainZoneKeys.classC.uuid;
1837 self.zones[self.keychainZoneID] = [[FakeCKZone alloc] initZone:self.keychainZoneID];
1838 self.keys[self.keychainZoneID] = nil;
1839 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1840 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:remotePeer1 zoneID:self.keychainZoneID];
1842 XCTAssertNotEqual(classCUUID, self.keychainZoneKeys.classC.uuid, @"Class C UUID should have changed");
1844 // Upon adding an item, we expect a failed OQO, then another OQO with the two items (encrypted correctly)
1845 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1847 [self expectCKModifyItemRecords:2
1848 currentKeyPointerRecords:1
1849 zoneID:self.keychainZoneID
1850 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1852 // We also expect a self share upload, once CKKS figures out the right key hierarchy
1853 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1855 [self addGenericPassword: @"data" account: @"account-delete-me-after-reset"];
1857 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1860 - (void)testRecoverFromRequestKeyRefetchWithoutRolling {
1861 // Simply requesting a key state refetch shouldn't roll the key hierarchy.
1863 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1865 // Spin up CKKS subsystem.
1866 [self startCKKSSubsystem];
1868 // Items should upload.
1869 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1870 [self addGenericPassword: @"data" account: @"account-delete-me"];
1871 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1873 [self waitForCKModifications];
1876 // CKKS should not roll the keys while progressing back to 'ready', but it will fetch once
1877 self.silentFetchesAllowed = false;
1878 [self expectCKFetch];
1880 [self.keychainView dispatchSyncWithAccountKeys: ^bool {
1881 [self.keychainView _onqueueKeyStateMachineRequestFetch];
1885 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
1886 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1889 - (void)testRecoverFromIncrementedCurrentKeyPointerEtag {
1890 // CloudKit sometimes reports the current key pointers have changed (etag mismatch), but their content hasn't.
1891 // In this case, CKKS shouldn't roll the TLK.
1893 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1895 // Spin up CKKS subsystem.
1896 [self startCKKSSubsystem];
1897 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // just to be sure it's fetched
1899 // Items should upload.
1900 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1901 [self addGenericPassword: @"data" account: @"account-delete-me"];
1902 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1904 [self waitForCKModifications];
1906 // Bump the etag on the class C current key record, but don't change any data
1907 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
1908 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
1909 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
1911 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
1912 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
1914 // Add another item. This write should fail, then CKKS should recover without rolling the key hierarchy or issuing a fetch.
1915 self.silentFetchesAllowed = false;
1916 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1917 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1918 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
1919 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1922 - (void)testRecoverMultipleItemsFromIncrementedCurrentKeyPointerEtag {
1923 // CloudKit sometimes reports the current key pointers have changed (etag mismatch), but their content hasn't.
1924 // In this case, CKKS shouldn't roll the TLK.
1925 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1927 // Spin up CKKS subsystem.
1928 [self startCKKSSubsystem];
1929 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // just to be sure it's fetched
1931 // Items should upload.
1932 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1933 [self addGenericPassword: @"data" account: @"account-delete-me"];
1934 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1936 [self waitForCKModifications];
1938 // Bump the etag on the class C current key record, but don't change any data
1939 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
1940 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
1941 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
1943 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
1944 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
1946 // Add another item. This write should fail, then CKKS should recover without rolling the key hierarchy or issuing a fetch.
1947 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
1948 secnotice("ckks", "releasing outgoing-queue hold");
1951 self.silentFetchesAllowed = false;
1952 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1953 [self expectCKModifyItemRecords:2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1954 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
1955 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
1957 [self.operationQueue addOperation: self.keychainView.holdOutgoingQueueOperation];
1958 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1961 - (void)testOnboardOldItemsCreatingKeyHierarchy {
1962 // 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
1964 // Test starts with nothing in CloudKit, and CKKS blocked. Add one item without a UUID...
1966 SecCKKSTestSetDisableAutomaticUUID(true);
1967 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1969 // and an item with a UUID...
1970 SecCKKSTestSetDisableAutomaticUUID(false);
1971 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1973 // We then expect an upload of the added items
1974 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1976 [self startCKKSSubsystem];
1977 [self performOctagonTLKUpload:self.ckksViews];
1979 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1982 - (void)testOnboardOldItemsWithExistingKeyHierarchy {
1983 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1985 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1986 [self addGenericPassword: @"data" account: @"account-delete-me"];
1988 [self startCKKSSubsystem];
1989 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1992 - (void)testOnboardOldItemsWithExistingKeyHierarchyExtantTLK {
1993 // Test starts key hierarchy in our fake CloudKit, the TLK arrived in the local keychain, and CKKS blocked.
1994 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1995 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1996 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1998 // Add one item without a UUID...
1999 SecCKKSTestSetDisableAutomaticUUID(true);
2000 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
2002 // and an item with a UUID...
2003 SecCKKSTestSetDisableAutomaticUUID(false);
2004 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
2006 // 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
2007 // We expect a single record to be uploaded.
2008 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2010 // Spin up CKKS subsystem.
2011 [self startCKKSSubsystem];
2013 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2016 - (void)testOnboardOldItemsWithExistingKeyHierarchyLateTLK {
2017 // Test starts key hierarchy in our fake CloudKit, and CKKS blocked.
2018 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2019 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
2020 self.keychainZone.flag = true;
2022 // Add one item without a UUID...
2023 SecCKKSTestSetDisableAutomaticUUID(true);
2024 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
2026 // and an item with a UUID...
2027 SecCKKSTestSetDisableAutomaticUUID(false);
2028 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
2030 // 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
2032 // Spin up CKKS subsystem.
2033 [self startCKKSSubsystem];
2034 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should have become waitfortlk");
2036 // Now, save the TLK to the keychain (to simulate it coming in via SOS).
2037 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2038 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2040 // We expect a single record to be uploaded.
2041 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2043 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2044 XCTAssertTrue(self.keychainZone.flag, "Keychain zone shouldn't have been reset");
2047 - (void)testResync {
2048 // We need to set up a desynced situation to test our resync.
2049 // First, let CKKS start up and send several items to CloudKit (that we'll then desync!)
2050 __block NSError* error = nil;
2052 // Test starts with keys in CloudKit (so we can create items later)
2053 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2054 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2055 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2057 [self addGenericPassword: @"data" account: @"first"];
2058 [self addGenericPassword: @"data" account: @"second"];
2059 [self addGenericPassword: @"data" account: @"third"];
2060 [self addGenericPassword: @"data" account: @"fourth"];
2061 [self addGenericPassword: @"data" account: @"fifth"];
2062 NSUInteger passwordCount = 5u;
2064 [self checkGenericPassword: @"data" account: @"first"];
2065 [self checkGenericPassword: @"data" account: @"second"];
2066 [self checkGenericPassword: @"data" account: @"third"];
2067 [self checkGenericPassword: @"data" account: @"fourth"];
2068 [self checkGenericPassword: @"data" account: @"fifth"];
2070 [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2072 [self startCKKSSubsystem];
2074 // Wait for uploads to happen
2075 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2076 [self waitForCKModifications];
2077 // One TLK share record
2078 XCTAssertEqual(self.keychainZone.currentDatabase.count, SYSTEM_DB_RECORD_COUNT+passwordCount+1, "Have SYSTEM_DB_RECORD_COUNT+passwordCount+1 objects in cloudkit");
2080 // Now, corrupt away!
2081 // Extract all passwordCount items for Corruption
2082 NSArray<CKRecord*>* items = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
2083 XCTAssertEqual(items.count, passwordCount, "Have %lu Items in cloudkit", (unsigned long)passwordCount);
2085 // For the first record, delete all traces of it from CKKS. But! it remains in local keychain.
2086 // Expected outcome: CKKS resyncs; item exists again.
2087 CKRecord* delete = items[0];
2088 NSString* deleteAccount = [[self decryptRecord: delete] objectForKey: (__bridge id) kSecAttrAccount];
2089 XCTAssertNotNil(deleteAccount, "received an account for the local delete object");
2091 __weak __typeof(self) weakSelf = self;
2092 [self.keychainView dispatchSync:^bool{
2093 __strong __typeof(weakSelf) strongSelf = weakSelf;
2094 XCTAssertNotNil(strongSelf, "self exists");
2096 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2098 [ckme deleteFromDatabase: &error];
2100 XCTAssertNil(error, "no error removing CKME");
2101 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2103 [oqe deleteFromDatabase: &error];
2105 XCTAssertNil(error, "no error removing OQE");
2106 CKKSIncomingQueueEntry* iqe = [CKKSIncomingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2108 [iqe deleteFromDatabase: &error];
2110 XCTAssertNil(error, "no error removing IQE");
2114 // For the second record, delete all traces of it from CloudKit.
2115 // Expected outcome: deleted locally
2116 CKRecord* remoteDelete = items[1];
2117 NSString* remoteDeleteAccount = [[self decryptRecord: remoteDelete] objectForKey: (__bridge id) kSecAttrAccount];
2118 XCTAssertNotNil(remoteDeleteAccount, "received an account for the remote delete object");
2120 [self.keychainZone deleteCKRecordIDFromZone: remoteDelete.recordID];
2121 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
2122 [database removeObjectForKey: remoteDelete.recordID];
2125 // The third record gets modified in CloudKit, but not locally.
2126 // Expected outcome: use the CloudKit version
2127 CKRecord* remoteDataChanged = items[2];
2128 NSMutableDictionary* remoteDataDictionary = [[self decryptRecord: remoteDataChanged] mutableCopy];
2129 NSString* remoteDataChangedAccount = [remoteDataDictionary objectForKey: (__bridge id) kSecAttrAccount];
2130 XCTAssertNotNil(remoteDataChangedAccount, "Received an account for the remote-data-changed object");
2131 remoteDataDictionary[(__bridge id) kSecValueData] = [@"CloudKitWins" dataUsingEncoding: NSUTF8StringEncoding];
2133 CKRecord* newData = [self newRecord: remoteDataChanged.recordID withNewItemData: remoteDataDictionary];
2134 [self.keychainZone addToZone: newData];
2135 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
2136 database[remoteDataChanged.recordID] = newData;
2139 // The fourth record stays in-sync. Good work, everyone!
2140 // Expected outcome: stays in-sync
2141 NSString* insyncAccount = [[self decryptRecord: items[3]] objectForKey: (__bridge id) kSecAttrAccount];
2142 XCTAssertNotNil(insyncAccount, "Received an account for the in-sync object");
2144 // The fifth record is updated locally, but CKKS didn't get the notification, and so the local CKMirror and CloudKit don't have it
2145 // Expected outcome: local change should be steamrolled by the cloud version
2146 CKRecord* localDataChanged = items[4];
2147 NSMutableDictionary* localDataDictionary = [[self decryptRecord: localDataChanged] mutableCopy];
2148 NSString* localDataChangedAccount = [localDataDictionary objectForKey: (__bridge id) kSecAttrAccount];
2150 [self updateGenericPassword:@"newpassword" account:localDataChangedAccount];
2151 [self checkGenericPassword:@"newpassword" account:localDataChangedAccount];
2154 // To make this more challenging, CK returns the refetch in multiple batches. This shouldn't affect the resync...
2155 CKServerChangeToken* ck1 = self.keychainZone.currentChangeToken;
2156 self.silentFetchesAllowed = false;
2157 [self expectCKFetch];
2158 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
2159 // Assert that the fetch is happening with the change token we paused at before
2160 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.keychainZoneID].previousServerChangeToken;
2161 if(changeToken && [changeToken isEqual:ck1]) {
2166 } runBeforeFinished:^{}];
2168 self.keychainZone.limitFetchTo = ck1;
2169 self.keychainZone.limitFetchError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkFailure userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:4]}];
2171 // The sixth record gets magically added to CloudKit, but CKKS has never heard of it
2172 // (emulates a lost record on the client, but that CloudKit already believes it's sent the record for)
2173 // Expected outcome: added to local keychain
2174 NSString* remoteOnlyAccount = @"remote-only";
2175 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount: remoteOnlyAccount];
2176 [self.keychainZone addToZone: ckr];
2177 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
2178 database[ckr.recordID] = ckr;
2181 ckksnotice("ckksresync", self.keychainView, "local delete: %@ %@", delete.recordID.recordName, deleteAccount);
2182 ckksnotice("ckksresync", self.keychainView, "Remote deletion: %@ %@", remoteDelete.recordID.recordName, remoteDeleteAccount);
2183 ckksnotice("ckksresync", self.keychainView, "Remote data changed: %@ %@", remoteDataChanged.recordID.recordName, remoteDataChangedAccount);
2184 ckksnotice("ckksresync", self.keychainView, "in-sync: %@ %@", items[3].recordID.recordName, insyncAccount);
2185 ckksnotice("ckksresync", self.keychainView, "local update: %@ %@", items[4].recordID.recordName, localDataChangedAccount);
2186 ckksnotice("ckksresync", self.keychainView, "Remote only: %@ %@", ckr.recordID.recordName, remoteOnlyAccount);
2188 CKKSSynchronizeOperation* resyncOperation = [self.keychainView resyncWithCloud];
2189 [resyncOperation waitUntilFinished];
2191 XCTAssertNil(resyncOperation.error, "No error during the resync operation");
2193 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2195 // Now do some checking. Remember, we don't know which record we corrupted, so use the parsed account variables to check.
2197 [self findGenericPassword: deleteAccount expecting: errSecSuccess];
2198 [self findGenericPassword: remoteDeleteAccount expecting: errSecItemNotFound];
2199 [self findGenericPassword: remoteDataChangedAccount expecting: errSecSuccess];
2200 [self findGenericPassword: insyncAccount expecting: errSecSuccess];
2201 [self findGenericPassword: localDataChangedAccount expecting: errSecSuccess];
2202 [self findGenericPassword: remoteOnlyAccount expecting: errSecSuccess];
2204 [self checkGenericPassword: @"data" account: deleteAccount];
2205 //[self checkGenericPassword: @"data" account: remoteDeleteAccount];
2206 [self checkGenericPassword: @"CloudKitWins" account: remoteDataChangedAccount];
2207 [self checkGenericPassword: @"data" account: insyncAccount];
2208 [self checkGenericPassword:@"data" account:localDataChangedAccount];
2209 [self checkGenericPassword: @"data" account: remoteOnlyAccount];
2211 [self.keychainView dispatchSync:^bool{
2212 __strong __typeof(weakSelf) strongSelf = weakSelf;
2213 XCTAssertNotNil(strongSelf, "self exists");
2215 CKKSMirrorEntry* ckme = nil;
2217 ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2218 XCTAssertNil(error);
2219 XCTAssertNotNil(ckme);
2221 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDelete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2222 XCTAssertNil(error);
2223 XCTAssertNil(ckme); // deleted!
2225 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDataChanged.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2226 XCTAssertNil(error);
2227 XCTAssertNotNil(ckme);
2229 ckme = [CKKSMirrorEntry tryFromDatabase:items[3].recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2230 XCTAssertNil(error);
2231 XCTAssertNotNil(ckme);
2233 ckme = [CKKSMirrorEntry tryFromDatabase:items[4].recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2234 XCTAssertNil(error);
2235 XCTAssertNotNil(ckme);
2237 ckme = [CKKSMirrorEntry tryFromDatabase:ckr.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2238 XCTAssertNil(error);
2239 XCTAssertNotNil(ckme);
2244 - (void)testResyncItemsMissingFromLocalKeychain {
2245 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2248 // one password correctly synced between local keychain and CloudKit
2249 // one password incorrectly disappeared from local keychain, but in mirror table
2250 // one password sitting in the outgoing queue
2251 // one password sitting in the incoming queue
2253 // Add and sync two passwords
2254 [self addGenericPassword: @"data" account: @"first"];
2255 [self addGenericPassword: @"data" account: @"second"];
2257 [self checkGenericPassword: @"data" account: @"first"];
2258 [self checkGenericPassword: @"data" account: @"second"];
2260 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2261 [self startCKKSSubsystem];
2262 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2263 [self waitForCKModifications];
2264 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2266 // Now, place an item in the outgoing queue
2268 //[self addGenericPassword: @"data" account: @"third"];
2269 //[self checkGenericPassword: @"data" account: @"third"];
2271 // Now, corrupt away!
2272 // Extract all passwordCount items for Corruption
2273 NSArray<CKRecord*>* items = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
2274 XCTAssertEqual(items.count, 2u, "Have %lu Items in cloudkit", (unsigned long)2u);
2276 // For the first record, surreptitiously remove from local keychain
2277 CKRecord* remove = items[0];
2278 NSString* removeAccount = [[self decryptRecord:remove] objectForKey:(__bridge id)kSecAttrAccount];
2279 XCTAssertNotNil(removeAccount, "received an account for the local delete object");
2281 NSURL* kcpath = (__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)@"keychain-2-debug.db");
2283 sqlite3_open([[kcpath path] UTF8String], &db);
2284 NSString* query = [NSString stringWithFormat:@"DELETE FROM genp WHERE uuid=\"%@\"", remove.recordID.recordName];
2285 char* sqlerror = NULL;
2286 XCTAssertEqual(SQLITE_OK, sqlite3_exec(db, [query UTF8String], NULL, NULL, &sqlerror), "SQL deletion shouldn't error");
2287 XCTAssertTrue(sqlerror == NULL, "No error string should have been returned: %s", sqlerror);
2289 sqlite3_free(sqlerror);
2294 // The second record is kept in-sync
2296 // Now, add an in-flight change (for record 3)
2297 [self holdCloudKitModifications];
2298 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2299 [self addGenericPassword:@"data" account:@"third"];
2300 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2302 // For the fourth, add a new record but prevent incoming queue processing
2303 self.keychainView.holdIncomingQueueOperation = [CKKSResultOperation named:@"hold-incoming" withBlock:^{}];
2305 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"fourth"];
2306 [self.keychainZone addToZone:ckr];
2307 [self.keychainView notifyZoneChange:nil];
2309 // Now, where are we....
2310 CKKSScanLocalItemsOperation* scanLocal = [self.keychainView scanLocalItems:@"test-scan"];
2311 [scanLocal waitUntilFinished];
2313 XCTAssertEqual(scanLocal.missingLocalItemsFound, 1u, "Should have found one missing item");
2315 // Allow everything to proceed
2316 [self releaseCloudKitModificationHold];
2317 [self.operationQueue addOperation:self.keychainView.holdIncomingQueueOperation];
2319 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2320 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2322 // And ensure that all four items are present again
2323 [self findGenericPassword: @"first" expecting: errSecSuccess];
2324 [self findGenericPassword: @"second" expecting: errSecSuccess];
2325 [self findGenericPassword: @"third" expecting: errSecSuccess];
2326 [self findGenericPassword: @"fourth" expecting: errSecSuccess];
2329 - (void)testScanItemsChangedInLocalKeychain {
2330 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2332 // Add and sync two passwords
2333 NSString* itemAccount = @"first";
2334 [self addGenericPassword:@"data" account:itemAccount];
2335 [self addGenericPassword:@"data" account:@"second"];
2337 [self checkGenericPassword:@"data" account:itemAccount];
2338 [self checkGenericPassword:@"data" account:@"second"];
2340 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2341 [self startCKKSSubsystem];
2342 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2343 [self waitForCKModifications];
2344 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2346 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
2347 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
2349 // Now, have CKKS miss an update
2351 [self updateGenericPassword:@"newpassword" account:itemAccount];
2352 [self checkGenericPassword:@"newpassword" account:itemAccount];
2355 // Now, where are we....
2356 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
2357 checkItem:[self checkPasswordBlock:self.keychainZoneID account:itemAccount password:@"newpassword"]];
2359 CKKSScanLocalItemsOperation* scanLocal = [self.keychainView scanLocalItems:@"test-scan"];
2360 [scanLocal waitUntilFinished];
2362 XCTAssertEqual(scanLocal.recordsAdded, 1u, "Should have added a single record");
2364 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2366 // And ensure that all four items are present again
2367 [self findGenericPassword: @"first" expecting: errSecSuccess];
2368 [self findGenericPassword: @"second" expecting: errSecSuccess];
2371 - (void)testResyncLocal {
2372 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2373 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2374 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2376 [self addGenericPassword: @"data" account: @"first"];
2377 [self addGenericPassword: @"data" account: @"second"];
2378 NSUInteger passwordCount = 2u;
2380 [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2381 [self startCKKSSubsystem];
2383 // Wait for uploads to happen
2384 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2385 [self waitForCKModifications];
2387 // Local resyncs shouldn't fetch clouds.
2388 self.silentFetchesAllowed = false;
2390 [self deleteGenericPassword:@"first"];
2391 [self deleteGenericPassword:@"second"];
2394 // And they're gone!
2395 [self findGenericPassword:@"first" expecting:errSecItemNotFound];
2396 [self findGenericPassword:@"second" expecting:errSecItemNotFound];
2398 CKKSLocalSynchronizeOperation* op = [self.keychainView resyncLocal];
2399 [op waitUntilFinished];
2400 XCTAssertNil(op.error, "Shouldn't be an error resyncing locally");
2402 // And they're back!
2403 [self checkGenericPassword: @"data" account: @"first"];
2404 [self checkGenericPassword: @"data" account: @"second"];
2407 - (void)testPlistRestoreResyncsLocal {
2408 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2409 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2410 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2412 [self addGenericPassword: @"data" account: @"first"];
2413 [self addGenericPassword: @"data" account: @"second"];
2414 NSUInteger passwordCount = 2u;
2416 [self checkGenericPassword: @"data" account: @"first"];
2417 [self checkGenericPassword: @"data" account: @"second"];
2419 [self expectCKModifyItemRecords:passwordCount currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2420 [self startCKKSSubsystem];
2422 // Wait for uploads to happen
2423 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2424 [self waitForCKModifications];
2427 // This 'restores' a plist keychain backup
2428 // That will kick off a local resync in CKKS, so hold that until we're ready...
2429 self.keychainView.holdLocalSynchronizeOperation = [CKKSResultOperation named:@"hold-local-synchronize" withBlock:^{}];
2431 // Local resyncs shouldn't fetch clouds.
2432 self.silentFetchesAllowed = false;
2434 CFErrorRef cferror = NULL;
2435 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
2436 CFErrorRef cfcferror = NULL;
2438 bool ret = SecServerImportKeychainInPlist(dbt, SecSecurityClientGet(), KEYBAG_NONE, KEYBAG_NONE,
2439 (__bridge CFDictionaryRef)@{}, kSecBackupableItemFilter, false, &cfcferror);
2441 XCTAssertNil(CFBridgingRelease(cfcferror), "Shouldn't error importing a 'backup'");
2442 XCTAssert(ret, "Importing a 'backup' should have succeeded");
2445 XCTAssertNil(CFBridgingRelease(cferror), "Shouldn't error mucking about in the db");
2447 // Restore is additive so original items stick around
2448 [self findGenericPassword:@"first" expecting:errSecSuccess];
2449 [self findGenericPassword:@"second" expecting:errSecSuccess];
2451 // Allow the local resync to continue...
2452 [self.operationQueue addOperation:self.keychainView.holdLocalSynchronizeOperation];
2453 [self.keychainView waitForOperationsOfClass:[CKKSLocalSynchronizeOperation class]];
2455 // Items are still here!
2456 [self checkGenericPassword: @"data" account: @"first"];
2457 [self checkGenericPassword: @"data" account: @"second"];
2460 - (void)testMultipleZoneAdd {
2461 // Bring up a new zone: we expect a key hierarchy upload.
2462 CKKSKeychainView* atvView = [self.injectedManager findOrCreateView:(id)kSecAttrViewHintAppleTV];
2463 [self.ckksViews addObject:atvView];
2464 CKRecordZoneID* appleTVZoneID = [[CKRecordZoneID alloc] initWithZoneName:(__bridge NSString*) kSecAttrViewHintAppleTV ownerName:CKCurrentUserDefaultName];
2466 // We also expect the view manager's notifyNewTLKsInKeychain call to fire once (after some delay)
2467 OCMExpect([self.mockCKKSViewManager notifyNewTLKsInKeychain]);
2469 // Let the horses loose
2470 [self startCKKSSubsystem];
2471 [self performOctagonTLKUpload:self.ckksViews];
2473 // We expect a single record to be uploaded to the 'keychain' view
2474 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2475 [self addGenericPassword: @"data" account: @"account-delete-me"];
2476 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2478 // We expect a single record to be uploaded to the 'atv' view
2479 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:appleTVZoneID];
2480 [self addGenericPassword: @"atv"
2481 account: @"tvaccount"
2482 viewHint:(__bridge NSString*) kSecAttrViewHintAppleTV
2483 access:(id)kSecAttrAccessibleAfterFirstUnlock
2484 expecting:errSecSuccess message:@"AppleTV view-hinted object"];
2486 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2488 OCMVerifyAllWithDelay(self.mockCKKSViewManager, 10);
2491 - (void)testMultipleZoneDelete {
2492 [self startCKKSSubsystem];
2494 // Bring up a new zone: we expect a key hierarchy and an item.
2495 CKKSKeychainView* atvView = [self.injectedManager findOrCreateView:(id)kSecAttrViewHintAppleTV];
2496 XCTAssertNotNil(atvView, "Should have a new ATV view");
2497 [self.ckksViews addObject:atvView];
2498 [self beginSOSTrustedViewOperation:atvView];
2499 CKRecordZoneID* appleTVZoneID = [[CKRecordZoneID alloc] initWithZoneName:(__bridge NSString*) kSecAttrViewHintAppleTV ownerName:CKCurrentUserDefaultName];
2501 [self performOctagonTLKUpload:self.ckksViews];
2503 // We expect a single record to be uploaded.
2504 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2505 [self addGenericPassword: @"data" account: @"account-delete-me"];
2506 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2508 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:appleTVZoneID];
2509 [self addGenericPassword: @"atv"
2510 account: @"tvaccount"
2511 viewHint:(__bridge NSString*) kSecAttrViewHintAppleTV
2512 access:(id)kSecAttrAccessibleAfterFirstUnlock
2513 expecting:errSecSuccess
2514 message:@"AppleTV view-hinted object"];
2515 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2517 // We expect a single record to be deleted from the ATV zone
2518 [self expectCKDeleteItemRecords: 1 zoneID:appleTVZoneID];
2519 [self deleteGenericPassword:@"tvaccount"];
2520 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2522 // Now we expect a single record to be deleted from the test zone
2523 [self expectCKDeleteItemRecords: 1 zoneID:self.keychainZoneID];
2524 [self deleteGenericPassword:@"account-delete-me"];
2525 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2528 - (void)testRestartWithoutRefetch {
2529 // Restarting the CKKS operation should check that it's been 15 minutes since the last fetch before it fetches again. Simulate this.
2530 [self startCKKSSubsystem];
2531 [self performOctagonTLKUpload:self.ckksViews];
2533 [self waitForCKModifications];
2534 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2536 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
2538 // Tear down the CKKS object and disallow fetches
2539 [self.keychainView halt];
2540 self.silentFetchesAllowed = false;
2542 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2543 [self beginSOSTrustedViewOperation:self.keychainView];
2544 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
2545 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2547 // Okay, cool, rad, now let's set the date to be very long ago and check that there's positively a fetch
2548 [self.keychainView halt];
2549 self.silentFetchesAllowed = false;
2551 [self.keychainView dispatchSync: ^bool {
2552 NSError* error = nil;
2553 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
2555 XCTAssertNil(error, "no error pulling ckse from database");
2556 XCTAssertNotNil(ckse, "received a ckse");
2558 ckse.lastFetchTime = [NSDate distantPast];
2559 [ckse saveToDatabase: &error];
2560 XCTAssertNil(error, "no error saving to database");
2564 [self expectCKFetch];
2565 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2566 [self beginSOSTrustedViewOperation:self.keychainView];
2567 [self.keychainView waitForKeyHierarchyReadiness];
2568 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2571 - (void)testRecoverFromZoneCreationFailure {
2572 // Fail the zone creation.
2573 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
2574 [self failNextZoneCreation:self.keychainZoneID];
2576 // Spin up CKKS subsystem.
2577 [self startCKKSSubsystem];
2579 // CKKS should figure it out, and fix it
2580 [self performOctagonTLKUpload:self.ckksViews];
2581 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2583 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2584 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2585 [self addGenericPassword: @"data" account: @"account-delete-me"];
2586 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2588 XCTAssertNil(self.zones[self.keychainZoneID].creationError, "Creation error was unset (and so CKKS probably dealt with the error");
2591 - (void)testRecoverFromZoneSubscriptionFailure {
2592 // Fail the zone subscription.
2593 [self failNextZoneSubscription:self.keychainZoneID];
2595 // Spin up CKKS subsystem.
2596 [self startCKKSSubsystem];
2598 // The CKKS subsystem should figure out the issue, and fix it before Octagon uploads its items
2599 [self performOctagonTLKUpload:self.ckksViews];
2601 [self.keychainView waitForKeyHierarchyReadiness];
2602 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2604 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2605 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2606 [self addGenericPassword: @"data" account: @"account-delete-me"];
2607 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2609 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
2612 - (void)testRecoverFromZoneSubscriptionFailureDueToZoneNotExisting {
2613 // This is different from testRecoverFromZoneSubscriptionFailure, since the zone is gone. CKKS must attempt to re-create the zone.
2615 // Silently fail the zone creation
2616 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
2617 [self failNextZoneCreationSilently:self.keychainZoneID];
2619 // Spin up CKKS subsystem.
2620 [self startCKKSSubsystem];
2622 // The CKKS subsystem should figure out the issue, and fix it.
2623 [self performOctagonTLKUpload:self.ckksViews];
2625 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
2626 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2628 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2629 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2630 [self addGenericPassword: @"data" account: @"account-delete-me"];
2631 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2633 XCTAssertFalse(self.zones[self.keychainZoneID].flag, "Zone flag was reset");
2634 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
2637 - (void)testRecoverFromDeletedTLKWithStashedTLK {
2638 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
2640 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2641 NSError* error = nil;
2644 [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
2645 XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
2647 // And delete the non-stashed version
2648 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
2649 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
2651 // Spin up CKKS subsystem.
2652 [self startCKKSSubsystem];
2654 [self.keychainView waitForKeyHierarchyReadiness];
2655 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2657 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2658 [self addGenericPassword: @"data" account: @"account-delete-me"];
2659 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2661 // CKKS should recreate the syncable TLK.
2662 [self checkNSyncableTLKsInKeychain: 1];
2665 - (void)testRecoverFromDeletedTLKWithStashedTLKUponRestart {
2666 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
2668 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2669 // Spin up CKKS subsystem.
2670 [self startCKKSSubsystem];
2671 [self.keychainView waitForKeyHierarchyReadiness];
2673 // Tear down the CKKS object
2674 [self.keychainView halt];
2676 NSError* error = nil;
2679 [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
2680 XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
2682 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
2683 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
2685 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2686 [self beginSOSTrustedViewOperation:self.keychainView];
2687 [self.keychainView waitForKeyHierarchyReadiness];
2688 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2690 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2691 [self addGenericPassword: @"data" account: @"account-delete-me"];
2692 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2694 // CKKS should recreate the syncable TLK.
2695 [self checkNSyncableTLKsInKeychain: 1];
2699 // <rdar://problem/49024967> Octagon: tests for CK exceptions out of cuttlefish
2700 - (void)testRecoverFromTLKWriteFailure {
2701 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
2702 // Test starts with nothing in CloudKit, and will fail the first TLK write.
2703 NSError* noNetwork = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}];
2704 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:noNetwork];
2706 // Spin up CKKS subsystem.
2707 [self startCKKSSubsystem];
2709 // The CKKS subsystem should figure out the issue, and fix it.
2710 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
2712 [self.keychainView waitForKeyHierarchyReadiness];
2713 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2715 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2716 [self addGenericPassword: @"data" account: @"account-delete-me"];
2717 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2719 // A network failure creating new TLKs shouldn't delete the 'failed' syncable one.
2720 [self checkNSyncableTLKsInKeychain: 2];
2724 // This test needs to be moved and rewritten now that Octagon handles TLK uploads
2725 // <rdar://problem/49024967> Octagon: tests for CK exceptions out of cuttlefish
2727 - (void)testRecoverFromTLKRace {
2728 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
2729 // Test starts with nothing in CloudKit, and will fail the first TLK write.
2730 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject: ^{
2731 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2734 // Spin up CKKS subsystem.
2735 [self startCKKSSubsystem];
2737 // The first TLK write should fail, and then our fake TLKs should be there in CloudKit.
2738 // It shouldn't write anything back up to CloudKit.
2739 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2741 // Now the TLKs arrive from the other device...
2742 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2743 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2744 [self.keychainView waitForKeyHierarchyReadiness];
2746 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2747 [self addGenericPassword: @"data" account: @"account-delete-me"];
2748 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2750 // A race failure creating new TLKs should delete the old syncable one.
2751 [self checkNSyncableTLKsInKeychain: 1];
2755 - (void)testRecoverFromNullCurrentKeyPointers {
2756 // The current key pointers in cloudkit shouldn't ever not exist if keys do. But, if they don't, CKKS must recover.
2758 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
2759 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2760 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2762 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
2763 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
2764 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2765 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2766 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
2768 // Spin up CKKS subsystem.
2769 [self startCKKSSubsystem];
2771 // The CKKS subsystem should figure out the issue, and fix it.
2772 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
2774 [self.keychainView waitForKeyHierarchyReadiness];
2776 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2779 - (void)testRecoverFromNoCurrentKeyPointers {
2780 // The current key pointers in cloudkit shouldn't ever point to nil. But, if they do, CKKS must recover.
2782 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
2783 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2784 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2786 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
2787 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentTLKPointer.storedCKRecord.recordID], "Deleted TLK pointer from zone");
2788 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassAPointer.storedCKRecord.recordID], "Deleted class a pointer from zone");
2789 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassCPointer.storedCKRecord.recordID], "Deleted class c pointer from zone");
2791 // Spin up CKKS subsystem.
2792 [self startCKKSSubsystem];
2794 // The CKKS subsystem should figure out the issue, and fix it.
2795 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
2797 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
2799 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2803 // <rdar://problem/49024967> Octagon: tests for CK exceptions out of cuttlefish
2804 - (void)testRecoverFromBadChangeTag {
2805 // 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.
2807 // Test starts with a broken key hierarchy in our fake CloudKit, but a (incorrectly) up-to-date change tag stored locally.
2808 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2809 SecCKKSTestSetDisableKeyNotifications(true); // Don't tell CKKS about this key material; we're pretending like this is a securityd restart
2810 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2811 SecCKKSTestSetDisableKeyNotifications(false);
2813 [self.keychainView dispatchSync: ^bool {
2814 NSError* error = nil;
2815 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainZoneID.zoneName];
2816 XCTAssertNotNil(ckse, "should have received a ckse");
2818 ckse.ckzonecreated = true;
2819 ckse.ckzonesubscribed = true;
2820 ckse.changeToken = self.keychainZone.currentChangeToken;
2822 [ckse saveToDatabase: &error];
2823 XCTAssertNil(error, "shouldn't have gotten an error saving to database");
2827 // The CKKS subsystem should try to write TLKs, but fail. It'll then upload a TLK share for the keys already in CloudKit
2828 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
2829 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2831 // Spin up CKKS subsystem.
2832 [self startCKKSSubsystem];
2833 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2835 // CKKS should then happily use the keys in CloudKit
2836 [self createClassCItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me"];
2837 [self createClassAItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me-class-a"];
2841 - (void)testRecoverFromDeletedKeysNewItem {
2842 [self startCKKSSubsystem];
2843 [self performOctagonTLKUpload:self.ckksViews];
2845 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
2847 // We expect a single class C record to be uploaded.
2848 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2849 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2851 [self addGenericPassword: @"data" account: @"account-delete-me"];
2852 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2854 [self waitForCKModifications];
2855 [self.keychainView waitUntilAllOperationsAreFinished];
2857 // Now, delete the local keys from the keychain (but leave the synced TLK)
2858 SecCKKSTestSetDisableKeyNotifications(true);
2859 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2860 (id)kSecClass : (id)kSecClassInternetPassword,
2861 (id)kSecUseDataProtectionKeychain : @YES,
2862 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2863 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
2864 }), @"Deleting local keys");
2865 SecCKKSTestSetDisableKeyNotifications(false);
2867 NSError* error = nil;
2868 [self.keychainZoneKeys.classC loadKeyMaterialFromKeychain:&error];
2869 XCTAssertNotNil(error, "Error loading class C key material from keychain");
2871 // We expect a single class C record to be uploaded.
2872 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2873 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2875 [self addGenericPassword: @"datadata" account: @"account-no-keys"];
2876 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2878 // We expect a single class A record to be uploaded.
2879 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
2880 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2881 [self addGenericPassword:@"asdf"
2882 account:@"account-class-A"
2884 access:(id)kSecAttrAccessibleWhenUnlocked
2885 expecting:errSecSuccess
2886 message:@"Adding class A item"];
2887 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2890 - (void)testRecoverFromDeletedKeysReceive {
2891 [self startCKKSSubsystem];
2892 [self performOctagonTLKUpload:self.ckksViews];
2894 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
2896 [self waitForCKModifications];
2897 [self.keychainView waitUntilAllOperationsAreFinished];
2899 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2901 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
2903 // Now, delete the local keys from the keychain (but leave the synced TLK)
2904 SecCKKSTestSetDisableKeyNotifications(true);
2905 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2906 (id)kSecClass : (id)kSecClassInternetPassword,
2907 (id)kSecUseDataProtectionKeychain : @YES,
2908 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2909 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
2910 }), @"Deleting local keys");
2911 SecCKKSTestSetDisableKeyNotifications(false);
2913 // Trigger a notification (with hilariously fake data)
2914 [self.keychainZone addToZone: ckr];
2915 [self.keychainView notifyZoneChange:nil];
2916 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2917 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2919 [self findGenericPassword: @"account0" expecting:errSecSuccess];
2922 - (void)testRecoverDeletedTLK {
2923 // If the TLK disappears halfway through, well, that's no good. But we should recover using TLK sharing
2925 [self startCKKSSubsystem];
2926 [self performOctagonTLKUpload:self.ckksViews];
2928 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
2930 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2931 [self waitForCKModifications];
2933 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
2934 [self.keychainView waitUntilAllOperationsAreFinished];
2936 // Now, delete the local keys from the keychain
2937 SecCKKSTestSetDisableKeyNotifications(true);
2938 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
2939 (id)kSecClass : (id)kSecClassInternetPassword,
2940 (id)kSecUseDataProtectionKeychain : @YES,
2941 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
2942 (id)kSecAttrSynchronizable : (id)kSecAttrSynchronizableAny,
2943 }), @"Deleting CKKS keys");
2944 SecCKKSTestSetDisableKeyNotifications(false);
2946 // Trigger a notification (with hilariously fake data)
2947 [self.keychainZone addToZone: ckr];
2948 [self.keychainView notifyZoneChange:nil];
2950 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2952 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should return to 'ready'");
2954 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // Do this again, to allow for non-atomic key state machinery switching
2956 [self findGenericPassword: @"account0" expecting:errSecSuccess];
2959 - (void)testRecoverMissingRolledKey {
2960 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2962 NSString* accountShouldExist = @"under-rolled-key";
2963 NSString* accountWillExist = @"under-rolled-key-later";
2964 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountShouldExist];
2965 [self.keychainZone addToZone: ckr];
2967 CKRecord* ckrAddedLater = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountWillExist];
2968 CKKSKey* pastClassCKey = self.keychainZoneKeys.classC;
2970 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2971 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2973 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2975 [self startCKKSSubsystem];
2976 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
2978 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2979 [self waitForCKModifications];
2981 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2982 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2983 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
2985 // Now, find and delete the class C key that ckrAddedLater is under
2986 NSError* error = nil;
2987 XCTAssertTrue([pastClassCKey deleteKeyMaterialFromKeychain:&error], "Should be able to delete old key material from keychain");
2988 XCTAssertNil(error, "Should be no error deleting old key material from keychain");
2990 [self.keychainZone addToZone:ckrAddedLater];
2991 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2993 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
2994 [self findGenericPassword:accountWillExist expecting:errSecSuccess];
2996 XCTAssertTrue([pastClassCKey loadKeyMaterialFromKeychain:&error], "Class C key should be back in the keychain");
2997 XCTAssertNil(error, "Should be no error loading key from keychain");
3000 - (void)testRecoverMissingRolledClassAKeyWhileLocked {
3001 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3003 NSString* accountShouldExist = @"under-rolled-key";
3004 NSString* accountWillExist = @"under-rolled-key-later";
3005 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountShouldExist key:self.keychainZoneKeys.classA];
3006 [self.keychainZone addToZone: ckr];
3008 CKRecord* ckrAddedLater = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountWillExist key:self.keychainZoneKeys.classA];
3009 CKKSKey* pastClassAKey = self.keychainZoneKeys.classA;
3011 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3012 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3014 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3016 [self startCKKSSubsystem];
3017 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
3019 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3020 [self waitForCKModifications];
3022 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
3023 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
3024 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
3026 // Now, find and delete the class C key that ckrAddedLater is under
3027 NSError* error = nil;
3028 XCTAssertTrue([pastClassAKey deleteKeyMaterialFromKeychain:&error], "Should be able to delete old key material from keychain");
3029 XCTAssertNil(error, "Should be no error deleting old key material from keychain");
3031 // now, lock the keychain
3032 self.aksLockState = true;
3033 [self.lockStateTracker recheck];
3035 [self.keychainZone addToZone:ckrAddedLater];
3036 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3038 // Item should still not exist due to the lock state....
3039 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
3040 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
3042 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], "Key state should have returned to readypendingunlock");
3044 self.aksLockState = false;
3045 [self.lockStateTracker recheck];
3048 [self.keychainView waitUntilAllOperationsAreFinished];
3049 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
3050 [self findGenericPassword:accountWillExist expecting:errSecSuccess];
3052 XCTAssertTrue([pastClassAKey loadKeyMaterialFromKeychain:&error], "Class A key should be back in the keychain");
3053 XCTAssertNil(error, "Should be no error loading key from keychain");
3056 - (void)testRecoverFromBadCurrentKeyPointer {
3057 // The current key pointers in cloudkit shouldn't ever point to missing entries. But, if they do, CKKS must recover.
3059 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
3060 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3061 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3063 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
3064 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
3065 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real tlk" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
3066 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class a key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
3067 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class c key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
3069 // Spin up CKKS subsystem.
3070 [self startCKKSSubsystem];
3072 // The CKKS subsystem should figure out the issue, and fix it (while uploading itself a TLK Share)
3073 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3075 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
3077 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3080 - (void)testRecoverFromIncorrectCurrentTLKPointer {
3081 // The current key pointers in cloudkit shouldn't ever point to wrong entries. But, if they do, CKKS must recover.
3083 // Test starts with a rolled hierarchy, and CKPs pointing to the wrong items
3084 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3085 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3087 CKKSCurrentKeyPointer* oldTLKCKP = self.keychainZoneKeys.currentTLKPointer;
3088 CKRecord* oldTLKPointer = [self.keychainZone.currentDatabase[self.keychainZoneKeys.currentTLKPointer.storedCKRecord.recordID] copy];
3090 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3091 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3093 ZoneKeys* newZoneKeys = [self.keychainZoneKeys copy];
3095 // And put the oldTLKPointer back
3096 [self.zones[self.keychainZoneID] addToZone:oldTLKPointer];
3097 self.keychainZoneKeys.currentTLKPointer = oldTLKCKP;
3099 // Make sure it stuck:
3100 XCTAssertNotEqualObjects(self.keychainZoneKeys.currentTLKPointer,
3101 newZoneKeys.currentTLKPointer,
3102 "current TLK pointer should now not point to proper TLK");
3104 // Spin up CKKS subsystem.
3105 [self startCKKSSubsystem];
3107 // The CKKS subsystem should figure out the issue, and fix it (while uploading itself a TLK Share)
3108 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
3110 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
3112 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3113 [self waitForCKModifications];
3115 XCTAssertEqualObjects(self.keychainZoneKeys.currentTLKPointer,
3116 newZoneKeys.currentTLKPointer,
3117 "current TLK pointer should now point to proper TLK");
3118 XCTAssertEqualObjects(self.keychainZoneKeys.currentClassAPointer,
3119 newZoneKeys.currentClassAPointer,
3120 "current Class A pointer should now point to proper Class A key");
3121 XCTAssertEqualObjects(self.keychainZoneKeys.currentClassCPointer,
3122 newZoneKeys.currentClassCPointer,
3123 "current Class C pointer should now point to proper Class C key");
3126 - (void)testRecoverFromDesyncedKeyRecordsViaResync {
3127 // We need to set up a desynced situation to test our resync.
3128 // First, let CKKS start up and send several items to CloudKit (that we'll then desync!)
3129 __block NSError* error = nil;
3131 // Test starts with keys in CloudKit (so we can create items later)
3132 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3133 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3134 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3136 [self addGenericPassword: @"data" account: @"first"];
3137 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
3139 [self startCKKSSubsystem];
3141 // Wait for uploads to happen
3142 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3143 [self waitForCKModifications];
3145 // Now, delete most of the key records are from on-disk, but the change token is not changed
3146 [self.keychainView dispatchSync:^bool{
3147 CKKSCurrentKeySet* keyset = [CKKSCurrentKeySet loadForZone:self.keychainZoneID];
3149 XCTAssertNotNil(keyset.currentTLKPointer, @"should be a TLK pointer");
3150 XCTAssertNotNil(keyset.currentClassAPointer, @"should be a class A pointer");
3151 XCTAssertNotNil(keyset.currentClassCPointer, @"should be a class C pointer");
3153 [keyset.currentTLKPointer deleteFromDatabase:&error];
3154 XCTAssertNil(error, "Should be no error deleting TLK pointer from database");
3155 [keyset.currentClassAPointer deleteFromDatabase:&error];
3156 XCTAssertNil(error, "Should be no error deleting class A pointer from database");
3158 XCTAssertNotNil(keyset.tlk, @"should be a TLK");
3159 XCTAssertNotNil(keyset.classA, @"should be a classA key");
3160 XCTAssertNotNil(keyset.classC, @"should be a classC key");
3162 [keyset.tlk deleteFromDatabase:&error];
3163 XCTAssertNil(error, "Should be no error deleting TLK from database");
3165 [keyset.classA deleteFromDatabase:&error];
3166 XCTAssertNil(error, "Should be no error deleting classA from database");
3168 [keyset.classC deleteFromDatabase:&error];
3169 XCTAssertNil(error, "Should be no error deleting classC from database");
3174 // A restart should realize there's an issue, and pause for help
3175 // 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
3176 [self.keychainView halt];
3177 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
3178 [self.keychainView beginCloudKitOperation];
3179 [self beginSOSTrustedViewOperation:self.keychainView];
3181 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"key state should enter 'waitfortlkcreation'");
3183 // But, a resync should fix you back up
3184 CKKSSynchronizeOperation* resyncOperation = [self.keychainView resyncWithCloud];
3185 [resyncOperation waitUntilFinished];
3186 XCTAssertNil(resyncOperation.error, "No error during the resync operation");
3188 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
3191 - (void)testRecoverFromCloudKitFetchFail {
3192 // Test starts with nothing in database, but one in our fake CloudKit.
3193 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3194 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
3196 // The first two CKRecordZoneChanges should fail with a 'network unavailable' error.
3197 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
3198 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
3200 // Spin up CKKS subsystem.
3201 [self startCKKSSubsystem];
3203 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3204 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3205 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3207 // We expect a single record to be uploaded
3208 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3209 [self addGenericPassword: @"data" account: @"account-delete-me"];
3210 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3212 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3213 [self addGenericPassword:@"asdf"
3214 account:@"account-class-A"
3216 access:(id)kSecAttrAccessibleWhenUnlocked
3217 expecting:errSecSuccess
3218 message:@"Adding class A item"];
3219 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3222 - (void)testRecoverFromCloudKitFetchNetworkFailAfterReady {
3223 // Test starts with nothing in database, but one in our fake CloudKit.
3224 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3226 // Spin up CKKS subsystem.
3227 [self startCKKSSubsystem];
3229 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
3230 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateReady, "CKKS entered ready");
3232 // Network is unavailable
3233 [self.reachabilityTracker setNetworkReachability:false];
3235 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
3236 [self.keychainZone addToZone:ckr];
3238 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3240 // Say network is available
3241 [self.reachabilityTracker setNetworkReachability:true];
3243 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3245 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3248 - (void)testRecoverFromCloudKitFetchNetworkFailBeforeReady {
3249 // Test starts with nothing in database, but one in our fake CloudKit.
3250 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3252 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
3253 [self.keychainZone addToZone:ckr];
3255 // Network is unavailable
3256 [self.reachabilityTracker setNetworkReachability:false];
3258 // Spin up CKKS subsystem.
3259 [self startCKKSSubsystem];
3261 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateInitializing] wait:20*NSEC_PER_SEC], "CKKS entered initializing");
3262 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateInitializing, "CKKS entered initializing");
3264 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3265 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3266 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3268 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3270 // Say network is available
3271 [self.reachabilityTracker setNetworkReachability:true];
3273 [self.keychainView waitUntilAllOperationsAreFinished];
3274 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3276 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3279 - (void)testWaitAfterCloudKitNetworkFailDuringOutgoingQueueOperation {
3280 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3282 [self startCKKSSubsystem];
3284 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered ready");
3286 // Network is now unavailable
3287 [self.reachabilityTracker setNetworkReachability:false];
3289 NSError* noNetwork = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{
3290 CKErrorRetryAfterKey: @(0.2),
3292 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:noNetwork];
3293 [self addGenericPassword: @"data" account: @"account-delete-me"];
3295 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3298 // Once network is available again, the write should happen
3299 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
3300 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3302 [self.reachabilityTracker setNetworkReachability:true];
3304 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3306 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3309 - (void)testRecoverFromCloudKitFetchFailWithDelay {
3310 // Test starts with nothing in database, but one in our fake CloudKit.
3311 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3312 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
3314 // The first CKRecordZoneChanges should fail with a 'delay' error.
3315 self.silentFetchesAllowed = false;
3316 [self.keychainZone failNextFetchWith:[[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorRequestRateLimited userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:4]}]];
3317 [self expectCKFetch];
3319 // Spin up CKKS subsystem.
3320 [self startCKKSSubsystem];
3322 // Ensure it doesn't fetch within these three seconds (if it does, an exception will throw).
3325 // Okay, you can fetch again.
3326 [self expectCKFetch];
3328 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3329 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3330 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3332 // We expect a single record to be uploaded
3333 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3334 [self addGenericPassword: @"data" account: @"account-delete-me"];
3335 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3338 - (void)testHandleZoneDeletedWhileFetching {
3339 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
3340 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3341 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3342 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3344 // 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)
3345 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorZoneNotFound userInfo:@{}]];
3347 [self startCKKSSubsystem];
3349 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"Key state should become 'ready'");
3352 - (void)testRecoverFromCloudKitOldChangeToken {
3353 // Test starts with nothing in database, but one in our fake CloudKit.
3354 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3356 // Spin up CKKS subsystem.
3357 [self startCKKSSubsystem];
3359 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3360 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3361 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3363 // We expect a single record to be uploaded
3364 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3365 [self addGenericPassword: @"data" account: @"account-delete-me"];
3366 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3368 // Delete all old database states, to destroy the change tag validity
3369 [self.keychainZone.pastDatabases removeAllObjects];
3371 // We expect a total local flush and refetch
3372 self.silentFetchesAllowed = false;
3373 [self expectCKFetch]; // one to fail with a CKErrorChangeTokenExpired error
3374 [self expectCKFetch]; // and one to succeed
3376 // Trigger a fake change notification
3377 [self.keychainView notifyZoneChange:nil];
3379 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3381 // And check that a new upload happens just fine.
3382 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3383 [self addGenericPassword:@"asdf"
3384 account:@"account-class-A"
3386 access:(id)kSecAttrAccessibleWhenUnlocked
3387 expecting:errSecSuccess
3388 message:@"Adding class A item"];
3389 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3392 - (void)testRecoverFromCloudKitUnknownDeviceStateRecord {
3393 // Test starts with nothing in database, but one in our fake CloudKit.
3394 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3395 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3396 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3398 // Save a new device state record with some fake etag
3399 [self.keychainView dispatchSync: ^bool {
3400 CKKSDeviceStateEntry* cdse = [[CKKSDeviceStateEntry alloc] initForDevice:self.ckDeviceID
3401 osVersion:@"fake-record"
3402 lastUnlockTime:[NSDate date]
3405 circlePeerID:self.mockSOSAdapter.selfPeer.peerID
3406 circleStatus:kSOSCCInCircle
3407 keyState:SecCKKSZoneKeyStateWaitForTLK
3409 currentClassAUUID:nil
3410 currentClassCUUID:nil
3411 zoneID:self.keychainZoneID
3412 encodedCKRecord:nil];
3413 XCTAssertNotNil(cdse, "Should have created a fake CDSE");
3414 CKRecord* record = [cdse CKRecordWithZoneID:self.keychainZoneID];
3415 XCTAssertNotNil(record, "Should have created a fake CDSE CKRecord");
3416 record.etag = @"fake etag";
3417 cdse.storedCKRecord = record;
3419 NSError* error = nil;
3420 [cdse saveToDatabase:&error];
3421 XCTAssertNil(error, @"No error saving cdse to database");
3426 // Spin up CKKS subsystem.
3427 [self startCKKSSubsystem];
3429 // We expect a record failure, since the device state record is broke
3430 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
3432 // And then we expect a clean write
3433 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
3434 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3436 [self addGenericPassword: @"data" account: @"account-delete-me"];
3437 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3440 - (void)testRecoverFromCloudKitUnknownItemRecord {
3441 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3443 // Spin up CKKS subsystem.
3444 [self startCKKSSubsystem];
3446 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3448 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
3449 [self.keychainZone addToZone:ckr];
3451 [self.keychainView notifyZoneChange:nil];
3452 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3454 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3456 // Delete the record from CloudKit, but miss the notification
3457 XCTAssertNil([self.keychainZone deleteCKRecordIDFromZone: ckr.recordID], "Deleting the record from fake CloudKit should succeed");
3459 // Expect a failed upload when we modify the item
3460 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
3461 [self updateGenericPassword:@"never seen again" account:@"account-delete-me"];
3462 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3464 [self.keychainView waitUntilAllOperationsAreFinished];
3466 // And the item should be disappeared from the local keychain
3467 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3470 - (void)testRecoverFromCloudKitUserDeletedZone {
3471 // Test starts with nothing in database, but one in our fake CloudKit.
3472 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3474 // Spin up CKKS subsystem.
3475 [self startCKKSSubsystem];
3477 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3478 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3479 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3481 // We expect a single record to be uploaded
3482 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3483 [self addGenericPassword: @"data" account: @"account-delete-me"];
3484 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3486 // The first CKRecordZoneChanges should fail with a 'CKErrorUserDeletedZone' error. This will cause a local reset, ending up with zone re-creation.
3487 self.zones[self.keychainZoneID] = nil; // delete the zone
3488 self.keys[self.keychainZoneID] = nil;
3489 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorUserDeletedZone userInfo:@{}]];
3491 // We expect CKKS to recreate the zone, then have octagon reupload the keys, and then the class C item upload
3492 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3494 [self.keychainView notifyZoneChange:nil];
3496 [self performOctagonTLKUpload:self.ckksViews];
3498 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3500 // And check that a new upload occurs.
3501 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3503 [self addGenericPassword:@"asdf"
3504 account:@"account-class-A"
3506 access:(id)kSecAttrAccessibleWhenUnlocked
3507 expecting:errSecSuccess
3508 message:@"Adding class A item"];
3509 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3512 - (void)testRecoverFromCloudKitZoneNotFoundWithoutZoneDeletion {
3513 // Test starts with nothing in database, but one in our fake CloudKit.
3514 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3515 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
3517 // Spin up CKKS subsystem.
3518 [self startCKKSSubsystem];
3520 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3521 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3522 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3524 // We expect a single record to be uploaded
3525 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3526 [self addGenericPassword: @"data" account: @"account-delete-me"];
3527 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3529 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
3531 [self waitForCKModifications];
3532 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
3534 // The next CKRecordZoneChanges will fail with a 'zone not found' error.
3535 self.zones[self.keychainZoneID] = nil; // delete the zone
3536 self.keys[self.keychainZoneID] = nil;
3538 // We expect CKKS to reset itself and recover, then have octagon upload the keys, and then the class C item upload
3539 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3541 [self.keychainView notifyZoneChange:nil];
3543 [self performOctagonTLKUpload:self.ckksViews];
3544 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3545 [self waitForCKModifications];
3547 // And check that a new upload occurs.
3548 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3550 [self addGenericPassword:@"asdf"
3551 account:@"account-class-A"
3553 access:(id)kSecAttrAccessibleWhenUnlocked
3554 expecting:errSecSuccess
3555 message:@"Adding class A item"];
3556 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3559 - (void)testRecoverFromCloudKitZoneNotFoundFetchBeforeSigninOccurs {
3560 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
3562 // Before CKKS sign-in, it receives a fetch rpc
3563 XCTestExpectation *fetchReturns = [self expectationWithDescription:@"fetch returned"];
3564 [self.injectedManager rpcFetchAndProcessChanges:nil reply:^(NSError *result) {
3565 XCTAssertNil(result, "Should be no error fetching and processing changes");
3566 [fetchReturns fulfill];
3569 [self startCKKSSubsystem];
3571 [self performOctagonTLKUpload:self.ckksViews];
3572 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
3574 // We expect a single record to be uploaded
3575 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
3576 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3577 [self addGenericPassword: @"data" account: @"account-delete-me"];
3578 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3580 // The fetch should have come back by now
3581 [self waitForExpectations: @[fetchReturns] timeout:5];
3584 - (void)testNoCloudKitAccount {
3585 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
3586 self.accountStatus = CKAccountStatusNoAccount;
3587 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3589 self.silentFetchesAllowed = false;
3590 [self startCKKSSubsystem];
3592 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3594 [self addGenericPassword: @"data" account: @"account-delete-me"];
3595 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3597 // simulate a NSNotification callback (but still logged out)
3598 self.accountStatus = CKAccountStatusNoAccount;
3599 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3601 // There should be no further uploads, even when we save keychain items
3602 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3603 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3605 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3606 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3608 // Test that there are no items in the database (since we never logged in)
3609 [self checkNoCKKSData: self.keychainView];
3612 - (void)testSACloudKitAccount {
3613 // Test starts with nothing in database and the user logged into CloudKit and in circle, but the account is not HSA2.
3614 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3616 self.accountStatus = CKAccountStatusAvailable;
3618 self.silentFetchesAllowed = false;
3620 // Octagon does not initialize the ckks views when not in an HSA2 account
3621 self.automaticallyBeginCKKSViewCloudKitOperation = false;
3622 [self startCKKSSubsystem];
3624 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3626 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3627 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS should enter 'loggedout'");
3629 // There should be no uploads, even when we save keychain items and enter/exit circle
3630 [self addGenericPassword: @"data" account: @"account-delete-me"];
3631 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3633 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3634 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3635 [self endSOSTrustedOperationForAllViews];
3636 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3638 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3639 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3640 [self beginSOSTrustedViewOperation:self.keychainView];
3641 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3643 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3644 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3646 // Test that there are no items in the database (since we never were in an HSA2 account)
3647 [self checkNoCKKSData: self.keychainView];
3650 - (void)testEarlyLogin
3652 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3654 // Octagon should initialize these views
3655 self.automaticallyBeginCKKSViewCloudKitOperation = true;
3657 self.accountStatus = CKAccountStatusAvailable;
3658 //[self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3660 [self startCKKSSubsystem];
3662 // CKKS should end up in 'waitfortlkcreation', as there's no trust and no TLKs
3663 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortlkcreation'");
3665 // Now, renotify the account status, and ensure that CKKS doesn't reenter 'initializing'
3666 CKKSCondition* initializing = self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateInitializing];
3668 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3670 XCTAssertNotEqual(0, [initializing wait:500*NSEC_PER_MSEC], "CKKS should not enter initializing when the device HSA status changes");
3673 - (void)testNoCircle {
3674 // Test starts with nothing in database and the user logged into CloudKit, but out of Circle.
3675 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3677 self.accountStatus = CKAccountStatusAvailable;
3679 [self startCKKSSubsystem];
3681 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3683 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3685 [self addGenericPassword: @"data" account: @"account-delete-me"];
3686 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3688 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortlkcreation'");
3690 // simulate a NSNotification callback (but still logged out)
3691 self.accountStatus = CKAccountStatusNoAccount;
3692 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3694 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'loggedout'");
3696 // There should be no further uploads, even when we save keychain items
3697 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3698 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3700 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3701 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3703 // Test that there are no items in the database (since we never logged in)
3704 [self checkNoCKKSData: self.keychainView];
3707 - (void)testCircleDepartAndRejoin {
3708 // Test starts with CKKS in ready
3709 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3710 [self startCKKSSubsystem];
3712 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
3714 // But then, trust departs
3715 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3716 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3717 [self endSOSTrustedOperationForAllViews];
3719 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortrust'");
3721 // There should be no further uploads, even when we save keychain items
3722 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3723 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3725 // Then trust returns. We expect two uploads
3726 [self expectCKModifyItemRecords:2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
3727 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3728 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3729 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3730 [self beginSOSTrustedViewOperation:self.keychainView];
3732 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3733 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3736 - (void)testCloudKitLogin {
3737 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
3738 self.accountStatus = CKAccountStatusNoAccount;
3739 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3741 // Before we inform CKKS of its account state....
3742 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
3744 [self startCKKSSubsystem];
3746 XCTAssertEqual(0, [self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], "Should have been told of a 'logout' event on startup");
3747 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event shouldn't have happened");
3748 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3750 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3752 // simulate a cloudkit login and NSNotification callback
3753 self.accountStatus = CKAccountStatusAvailable;
3754 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3756 // No writes yet, since we're not in circle
3757 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortlkcreation'");
3758 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3760 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3761 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3762 [self beginSOSTrustedOperationForAllViews];
3764 [self performOctagonTLKUpload:self.ckksViews];
3766 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3767 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3768 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3770 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3771 [self waitForCKModifications];
3773 // We expect a single class C record to be uploaded.
3774 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3775 [self addGenericPassword: @"data" account: @"account-delete-me"];
3777 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3778 [self waitForCKModifications];
3781 - (void)testCloudKitLogoutLogin {
3782 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
3783 [self startCKKSSubsystem];
3784 [self performOctagonTLKUpload:self.ckksViews];
3785 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3786 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3787 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3789 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3790 [self waitForCKModifications];
3792 // We expect a single class C record to be uploaded.
3793 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3794 [self addGenericPassword: @"data" account: @"account-delete-me"];
3796 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3797 [self waitForCKModifications];
3799 // simulate a cloudkit logout and NSNotification callback
3800 self.accountStatus = CKAccountStatusNoAccount;
3801 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3802 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3803 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3804 [self endSOSTrustedOperationForAllViews];
3806 // Test that there are no items in the database after logout
3807 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
3808 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
3809 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3810 [self checkNoCKKSData: self.keychainView];
3812 // There should be no further uploads, even when we save keychain items
3813 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3814 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3816 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3817 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3818 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
3820 // simulate a cloudkit login
3821 // 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
3822 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
3824 self.accountStatus = CKAccountStatusAvailable;
3825 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3827 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3828 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3829 [self beginSOSTrustedViewOperation:self.keychainView];
3831 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3832 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3833 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3835 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3837 // Let everything settle...
3838 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3839 [self waitForCKModifications];
3842 self.accountStatus = CKAccountStatusNoAccount;
3843 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3845 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3846 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3847 [self endSOSTrustedOperationForAllViews];
3849 // Test that there are no items in the database after logout
3850 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
3851 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
3852 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3853 [self checkNoCKKSData: self.keychainView];
3855 // There should be no further uploads, even when we save keychain items
3856 [self addGenericPassword: @"data" account: @"account-delete-me-5"];
3857 [self addGenericPassword: @"data" account: @"account-delete-me-6"];
3859 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3860 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3862 // simulate a cloudkit login
3863 // 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
3864 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
3866 self.accountStatus = CKAccountStatusAvailable;
3867 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3869 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3870 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3871 [self beginSOSTrustedViewOperation:self.keychainView];
3873 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3874 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3875 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3877 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3879 // Let everything settle...
3880 [self.keychainView waitUntilAllOperationsAreFinished];
3881 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3884 self.accountStatus = CKAccountStatusNoAccount;
3885 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3887 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
3888 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3889 [self endSOSTrustedOperationForAllViews];
3891 // Test that there are no items in the database after logout
3892 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
3893 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
3894 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3895 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
3896 [self checkNoCKKSData: self.keychainView];
3898 // Force zone into error state
3899 self.keychainView.keyHierarchyState = SecCKKSZoneKeyStateError;
3901 self.accountStatus = CKAccountStatusAvailable;
3902 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3904 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3905 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3906 [self beginSOSTrustedViewOperation:self.keychainView];
3908 XCTestExpectation *operationRun = [self expectationWithDescription:@"operation run"];
3909 NSOperation* op = [NSBlockOperation named:@"test" withBlock:^{
3910 [operationRun fulfill];
3913 [op addDependency:self.keychainView.keyStateReadyDependency];
3914 [self.operationQueue addOperation:op];
3916 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
3917 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
3918 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3920 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3921 [self waitForExpectations: @[operationRun] timeout:10];
3922 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3925 - (void)testCloudKitLogoutDueToGreyMode {
3926 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
3927 [self startCKKSSubsystem];
3928 [self performOctagonTLKUpload:self.ckksViews];
3929 XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "Should have been told of a 'login'");
3930 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:50*NSEC_PER_MSEC], "'logout' event should be reset");
3931 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3933 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
3935 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3936 [self waitForCKModifications];
3938 // simulate a cloudkit grey mode switch and NSNotification callback. CKKS should treat this as a logout
3939 self.iCloudHasValidCredentials = false;
3940 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3942 // Test that there are no items in the database after logout
3943 XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "Should have been told of a 'logout'");
3944 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:50*NSEC_PER_MSEC], "'login' event should be reset");
3945 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3946 [self checkNoCKKSData: self.keychainView];
3947 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
3949 // There should be no further uploads, even when we save keychain items
3950 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
3951 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
3953 [self.keychainView waitUntilAllOperationsAreFinished];
3954 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3956 // Also, fetches shouldn't occur
3957 self.silentFetchesAllowed = false;
3958 NSOperation* op = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
3959 CKKSResultOperation* timeoutOp = [CKKSResultOperation named:@"timeout" withBlock:^{}];
3960 [timeoutOp addDependency:op];
3961 [timeoutOp timeout:4*NSEC_PER_SEC];
3962 [self.operationQueue addOperation:timeoutOp];
3963 [timeoutOp waitUntilFinished];
3965 // CloudKit figures its life out. We expect the two passwords from before to be uploaded
3966 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
3967 self.silentFetchesAllowed = true;
3968 self.iCloudHasValidCredentials = true;
3969 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
3971 XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "Should have been told of a 'login'");
3972 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:50*NSEC_PER_MSEC], "'logout' event should be reset");
3973 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
3974 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3976 // And fetching still works!
3977 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D00" withAccount:@"account0"]];
3978 [self.keychainView notifyZoneChange:nil];
3979 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3980 [self findGenericPassword: @"account0" expecting:errSecSuccess];
3981 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
3984 - (void)testCloudKitLoginRace {
3985 // Test starts with nothing in database, and 'in circle', but securityd hasn't received notification if we're logged into CloudKit.
3986 // CKKS should call handleLogout, as the CK account is not present.
3988 // note: don't unblock the ck account state object yet...
3990 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
3991 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
3993 // Add a keychain item, and make sure it doesn't upload yet.
3994 [self addGenericPassword: @"data" account: @"account-delete-me"];
3995 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3996 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'loggedout'");
3998 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4000 // Now that we're here (and logged out), bring the account up
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"]];
4005 self.accountStatus = CKAccountStatusAvailable;
4006 [self startCKKSSubsystem];
4007 [self performOctagonTLKUpload:self.ckksViews];
4009 // simulate another NSNotification callback
4010 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4012 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4013 [self waitForCKModifications];
4015 // Make sure new items upload too
4016 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4017 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4018 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4020 [self.keychainView waitUntilAllOperationsAreFinished];
4021 [self waitForCKModifications];
4022 [self.keychainView halt];
4025 - (void)testDontLogOutIfBeforeFirstUnlock {
4027 // test starts as if a previously logged-in device has just rebooted
4028 self.aksLockState = true;
4029 self.accountStatus = CKAccountStatusAvailable;
4031 // This is the original state of the account tracker
4032 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCError error:nil];
4033 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4035 // And this is what the first circle status fetch will actually return
4036 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"]];
4037 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4039 XCTAssertEqual(self.accountStateTracker.currentComputedAccountStatus, CKKSAccountStatusUnknown, "Account tracker status should just be 'unknown'");
4040 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKKS should not yet know the CK account state");
4042 [self startCKKSSubsystem];
4044 XCTAssertEqual(0, [self.keychainView.loggedIn wait:8*NSEC_PER_SEC], "'login' event should have happened");
4045 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:10*NSEC_PER_MSEC], "Should not have been told of a CK 'logout' event on startup");
4046 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:1*NSEC_PER_SEC], "CKKS should know the account state");
4048 // And assume another CK status change
4049 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4050 XCTAssertEqual(self.accountStateTracker.currentComputedAccountStatus, CKKSAccountStatusUnknown, "Account tracker status should just be 'no account'");
4051 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKKS should know the CK account state");
4053 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
4055 self.aksLockState = false;
4057 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4058 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4059 [self beginSOSTrustedViewOperation:self.keychainView];
4061 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
4062 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
4063 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4065 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4066 [self waitForCKModifications];
4068 // We expect a single class C record to be uploaded.
4069 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4070 [self addGenericPassword: @"data" account: @"account-delete-me"];
4072 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4073 [self waitForCKModifications];*/
4076 - (void)testSyncableItemsAddedWhileLoggedOut {
4077 // Test that once CKKS is up and 'logged out', nothing happens when syncable items are added
4078 self.accountStatus = CKAccountStatusNoAccount;
4079 [self startCKKSSubsystem];
4081 XCTAssertEqual([self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], 0, "CKKS should be told that it's logged out");
4083 // CKKS shouldn't decide to poke its state machine, but it should still send the notification
4084 XCTestExpectation* viewChangeNotification = [self expectChangeForView:self.keychainZoneID.zoneName];
4086 // Reject all attempts to trigger a state machine update
4087 id pokeKeyStateMachineScheduler = OCMClassMock([CKKSNearFutureScheduler class]);
4088 OCMReject([pokeKeyStateMachineScheduler trigger]);
4089 self.keychainView.pokeKeyStateMachineScheduler = pokeKeyStateMachineScheduler;
4091 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4093 [self waitForExpectations:@[viewChangeNotification] timeout:8];
4094 [pokeKeyStateMachineScheduler stopMocking];
4097 - (void)testUploadSyncableItemsAddedWhileUntrusted {
4098 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4099 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4101 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
4103 [self startCKKSSubsystem];
4105 XCTAssertEqual([self.keychainView.loggedIn wait:500*NSEC_PER_MSEC], 0, "CKKS should be told that it's logged in");
4107 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered waitfortrust");
4108 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4110 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4114 NSError* error = nil;
4115 NSDictionary* currentOQEs = [CKKSOutgoingQueueEntry countsByStateInZone:self.keychainZoneID error:&error];
4116 XCTAssertNil(error, "Should be no error coutning OQEs");
4117 XCTAssertEqual(0, currentOQEs.count, "Should be no OQEs");
4119 // Now, insert a restart to simulate securityd restarting (and throwing away all pending operations), then a real sign in
4120 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4121 [self endSOSTrustedViewOperation:self.keychainView];
4122 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered waitfortrust");
4124 // Okay! Upon sign in, this item should be uploaded
4125 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4126 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4128 [self putSelfTLKSharesInCloudKit:self.keychainZoneID];
4129 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4130 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4131 [self beginSOSTrustedViewOperation:self.keychainView];
4133 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4134 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4137 // Note that this test assumes that the keychainView object was created at daemon restart.
4138 // I don't really know how to write a test for that...
4139 - (void)testSyncableItemAddedOnDaemonRestartBeforeCloudKitAccountKnown {
4140 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
4141 [self startCKKSSubsystem];
4143 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4146 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4147 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4148 [self beginSOSTrustedViewOperation:self.keychainView];
4150 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4151 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:100*NSEC_PER_MSEC], "CKKS should still have no idea what the account state is");
4152 XCTAssertEqual(self.keychainView.accountStatus, CKKSAccountStatusUnknown, "Account status should be unknown");
4153 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
4155 [self.keychainView beginCloudKitOperation];
4157 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4158 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4159 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4160 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4163 - (void)testSyncableItemModifiedOnDaemonRestartBeforeCloudKitAccountKnown {
4164 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
4165 [self startCKKSSubsystem];
4167 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4169 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4170 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4171 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4172 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4175 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4176 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4177 [self beginSOSTrustedViewOperation:self.keychainView];
4179 [self updateGenericPassword:@"newdata" account: @"account-delete-me-2"];
4180 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:100*NSEC_PER_MSEC], "CKKS should still have no idea what the account state is");
4181 XCTAssertEqual(self.keychainView.accountStatus, CKKSAccountStatusUnknown, "Account status should be unknown");
4182 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
4184 [self.keychainView beginCloudKitOperation];
4186 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4187 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4188 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4189 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4192 - (void)testSyncableItemDeletedOnDaemonRestartBeforeCloudKitAccountKnown {
4193 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
4194 [self startCKKSSubsystem];
4196 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4198 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4199 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4200 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4201 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4204 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4205 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4206 [self beginSOSTrustedViewOperation:self.keychainView];
4208 [self deleteGenericPassword:@"account-delete-me-2"];
4209 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:100*NSEC_PER_MSEC], "CKKS should still have no idea what the account state is");
4210 XCTAssertEqual(self.keychainView.accountStatus, CKKSAccountStatusUnknown, "Account status should be unknown");
4211 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
4213 [self.keychainView beginCloudKitOperation];
4215 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
4216 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4217 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4220 - (void)testNotStuckAfterReset {
4221 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
4223 XCTestExpectation *operationRun = [self expectationWithDescription:@"operation run"];
4224 NSOperation* op = [NSBlockOperation named:@"test" withBlock:^{
4225 [operationRun fulfill];
4228 [op addDependency:self.keychainView.keyStateReadyDependency];
4229 [self.operationQueue addOperation:op];
4231 // And handle a spurious logout
4232 [self.keychainView handleCKLogout];
4234 [self startCKKSSubsystem];
4236 [self waitForExpectations: @[operationRun] timeout:20];
4239 - (void)testCKKSControlBringup {
4240 NSXPCInterface *interface = CKKSSetupControlProtocol([NSXPCInterface interfaceWithProtocol:@protocol(CKKSControlProtocol)]);
4241 XCTAssertNotNil(interface, "Received a configured CKKS interface");
4244 - (void)testMetricsUpload {
4246 XCTestExpectation *upload = [self expectationWithDescription:@"CAMetrics"];
4247 XCTestExpectation *collection = [self expectationWithDescription:@"CAMetrics"];
4249 id saMock = OCMClassMock([SecCoreAnalytics class]);
4250 OCMStub([saMock sendEvent:[OCMArg any] event:[OCMArg any]]).andDo(^(NSInvocation* invocation) {
4254 NSString *sampleSampler = @"stuff";
4256 [[CKKSAnalytics logger] AddMultiSamplerForName:sampleSampler withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSDictionary<NSString *,NSNumber *> *{
4257 [collection fulfill];
4258 return @{ @"hej" : @1 };
4262 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
4263 [self startCKKSSubsystem];
4265 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
4267 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
4268 deletedRecordTypeCounts:nil
4269 zoneID:self.keychainZoneID
4270 checkModifiedRecord:nil
4271 runAfterModification:nil];
4273 [self.injectedManager xpc24HrNotification];
4275 [self waitForExpectations: @[upload, collection] timeout:10];
4276 [[CKKSAnalytics logger] removeMultiSamplerForName:sampleSampler];
4279 - (void)testSaveManyTLKShares {
4280 // Spin up CKKS subsystem.
4281 [self startCKKSSubsystem];
4283 [self performOctagonTLKUpload:self.ckksViews];
4284 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4286 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
4288 NSMutableArray<CKKSSOSSelfPeer*>* peers = [NSMutableArray array];
4290 for(int i = 0; i < 20; i++) {
4291 CKKSSOSSelfPeer* untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:[NSString stringWithFormat:@"untrusted-peer-%d", i]
4292 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
4293 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
4294 viewList:self.managedViewList];
4296 [peers addObject:untrustedPeer];
4299 NSMutableArray<CKRecord*>* tlkShareRecords = [NSMutableArray array];
4301 for(CKKSSOSSelfPeer* peer1 in peers) {
4302 for(CKKSSOSSelfPeer* peer2 in peers) {
4303 NSError* error = nil;
4304 CKKSTLKShareRecord* share = [CKKSTLKShareRecord share:self.keychainZoneKeys.tlk
4310 XCTAssertNil(error, "Should have been no error sharing a CKKSKey");
4311 XCTAssertNotNil(share, "Should be able to create a share");
4313 CKRecord* shareRecord = [share CKRecordWithZoneID:self.keychainZoneID];
4314 [tlkShareRecords addObject:shareRecord];
4318 [self measureBlock:^{
4319 [self.keychainView dispatchSyncWithAccountKeys:^bool{
4320 for(CKRecord* record in tlkShareRecords) {
4321 [self.keychainView _onqueueCKRecordChanged:record resync:false];
4328 - (void)testReceiveNotificationDuringLaunch {
4329 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
4331 [self holdCloudKitModifyRecordZones];
4333 // Spin up CKKS subsystem.
4334 [self startCKKSSubsystem];
4336 CKKSCondition* fetcherCondition = self.keychainView.zoneChangeFetcher.fetchScheduler.liveRequestReceived;
4338 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
4340 [self.keychainView notifyZoneChange:nil];
4342 XCTAssertNotEqual(0, [fetcherCondition wait:(3 * NSEC_PER_SEC)], "not supposed to get a fetch data");
4344 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
4345 self.silentFetchesAllowed = false;
4346 [self expectCKFetch];
4347 [self releaseCloudKitModifyRecordZonesHold];
4349 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4350 OCMVerifyAllWithDelay(self.mockDatabase, 20);