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/CKKSStates.h"
48 #import "keychain/ckks/CKKSSynchronizeOperation.h"
49 #import "keychain/ckks/CKKSViewManager.h"
50 #import "keychain/ckks/CKKSZoneStateEntry.h"
51 #import "keychain/ckks/CKKSManifest.h"
52 #import "keychain/ckks/CKKSAnalytics.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 "keychain/ot/ObjCImprovements.h"
61 #import <utilities/SecCoreAnalytics.h>
64 @interface CKKSLockStateTracker ()
65 @property (nullable) NSDate* lastUnlockedTime;
68 @implementation CloudKitKeychainSyncingTests
72 - (void)testBringupToKeyStateReady {
73 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
74 [self startCKKSSubsystem];
76 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
80 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
82 // We expect a single record to be uploaded.
83 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
85 [self startCKKSSubsystem];
86 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
88 [self addGenericPassword: @"data" account: @"account-delete-me"];
90 OCMVerifyAllWithDelay(self.mockDatabase, 20);
93 - (void)testActiveTLKs {
94 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
96 // We expect a single record to be uploaded.
97 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
99 [self startCKKSSubsystem];
100 [self addGenericPassword: @"data" account: @"account-delete-me"];
102 OCMVerifyAllWithDelay(self.mockDatabase, 20);
104 NSError* localError = nil;
105 NSArray<CKKSKeychainBackedKey*>* tlks = [[CKKSViewManager manager] currentTLKsFilteredByPolicy:NO error:&localError];
106 XCTAssertNil(localError, "Should have no error fetching current TLKs");
108 XCTAssertEqual([tlks count], (NSUInteger)1, "Should have one TLK");
109 XCTAssertEqualObjects(tlks[0].zoneID.zoneName, @"keychain", "should have a TLK for keychain");
111 XCTAssertEqualObjects(tlks[0].uuid, self.keychainZoneKeys.tlk.uuid, "should have the TLK matching cloudkit");
114 - (void)testActiveTLKsWhenMissing {
115 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
116 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
118 [self startCKKSSubsystem];
119 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should have arrived at waitfortlk");
121 NSError* localError = nil;
122 NSArray<CKKSKeychainBackedKey*>* tlks = [[CKKSViewManager manager] currentTLKsFilteredByPolicy:NO error:&localError];
123 XCTAssertNil(localError, "Should have no error fetching current TLKs");
125 XCTAssertEqual([tlks count], (NSUInteger)0, "Should have zero TLKs");
128 - (void)testActiveTLKsWhenLocked {
129 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
131 [self startCKKSSubsystem];
132 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
134 self.aksLockState = true;
135 [self.lockStateTracker recheck];
137 NSError* localError = nil;
138 NSArray<CKKSKeychainBackedKey*>* tlks = [[CKKSViewManager manager] currentTLKsFilteredByPolicy:NO error:&localError];
139 XCTAssertNotNil(localError, "Should have an error fetching current TLKs");
141 XCTAssertEqual([tlks count], (NSUInteger)0, "Should have zero TLKs");
144 - (void)testAddMultipleItems {
145 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
146 [self startCKKSSubsystem];
148 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
149 [self addGenericPassword: @"data" account: @"account-delete-me"];
150 OCMVerifyAllWithDelay(self.mockDatabase, 20);
152 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
153 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
154 OCMVerifyAllWithDelay(self.mockDatabase, 20);
156 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
157 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
158 OCMVerifyAllWithDelay(self.mockDatabase, 20);
161 - (void)testAddItemWithoutUUID {
162 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
163 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
164 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
165 [self saveTLKMaterialToKeychain:self.keychainZoneID];
167 [self startCKKSSubsystem];
169 [self.keychainView waitUntilAllOperationsAreFinished];
171 // We expect an upload of the added item, once CKKS finds the UUID-less item and fixes it
172 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
174 SecCKKSTestSetDisableAutomaticUUID(true);
175 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
176 SecCKKSTestSetDisableAutomaticUUID(false);
178 [self findGenericPassword:@"account-delete-me-no-UUID" expecting:errSecSuccess];
180 OCMVerifyAllWithDelay(self.mockDatabase, 20);
183 - (void)testModifyItem {
184 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
186 NSString* account = @"account-delete-me";
188 [self startCKKSSubsystem];
190 // We expect a single record to be uploaded.
191 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
192 [self addGenericPassword: @"data" account: account];
193 OCMVerifyAllWithDelay(self.mockDatabase, 20);
195 // And then modified.
196 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
197 [self updateGenericPassword: @"otherdata" account:account];
198 OCMVerifyAllWithDelay(self.mockDatabase, 20);
201 - (void)testModifyItemImmediately {
202 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
203 NSString* account = @"account-delete-me";
205 [self startCKKSSubsystem];
206 [self holdCloudKitModifications];
208 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
209 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
210 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
211 [self addGenericPassword: @"data" account: account];
212 OCMVerifyAllWithDelay(self.mockDatabase, 20);
214 // Right now, the write in CloudKit is pending. Make the local modification...
215 [self updateGenericPassword: @"otherdata" account:account];
217 // And then schedule the update
218 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
219 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
220 [self releaseCloudKitModificationHold];
222 OCMVerifyAllWithDelay(self.mockDatabase, 20);
225 - (void)testModifyItemPrimaryKey {
226 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
228 NSString* account = @"account-delete-me";
230 [self startCKKSSubsystem];
232 // We expect a single record to be uploaded.
233 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
234 [self addGenericPassword: @"data" account: account];
235 OCMVerifyAllWithDelay(self.mockDatabase, 20);
237 // And then modified. Since we're changing the "primary key", we expect to delete the old record and upload a new one.
238 [self expectCKModifyItemRecords:1 deletedRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:nil];
239 [self updateAccountOfGenericPassword: @"new-account-delete-me" account:account];
240 OCMVerifyAllWithDelay(self.mockDatabase, 20);
243 - (void)testModifyItemDuringReencrypt {
244 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
245 NSString* account = @"account-delete-me";
247 [self startCKKSSubsystem];
248 [self holdCloudKitModifications];
250 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
251 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
252 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
253 [self addGenericPassword: @"data" account: account];
254 OCMVerifyAllWithDelay(self.mockDatabase, 20);
256 // Right now, the write in CloudKit is pending. Make the local modification...
257 [self updateGenericPassword: @"otherdata" account:account];
259 // And then schedule the update
260 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
261 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
263 // Stop the reencrypt operation from happening
264 self.keychainView.holdReencryptOutgoingItemsOperation = [CKKSGroupOperation named:@"reencrypt-hold" withBlock: ^{
265 ckksnotice_global("ckks", "releasing reencryption hold");
268 // The cloudkit operation finishes, letting the next OQO proceed (and set up the reencryption operation)
269 [self releaseCloudKitModificationHold];
271 // And wait for this to finish...
272 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
273 // And once more to quiesce.
274 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
276 // Pause outgoing queue operations to ensure the reencryption operation runs first
277 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
278 ckksnotice_global("ckks", "releasing outgoing-queue hold");
281 // Run the reencrypt items operation to completion.
282 [self.operationQueue addOperation: self.keychainView.holdReencryptOutgoingItemsOperation];
283 [self.keychainView waitForOperationsOfClass:[CKKSReencryptOutgoingItemsOperation class]];
285 [self.operationQueue addOperation: self.keychainView.holdOutgoingQueueOperation];
287 OCMVerifyAllWithDelay(self.mockDatabase, 20);
288 [self.keychainView waitUntilAllOperationsAreFinished];
289 [self waitForCKModifications];
292 - (void)testModifyItemBeforeReencrypt {
293 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
294 NSString* account = @"account-delete-me";
296 [self startCKKSSubsystem];
297 [self holdCloudKitModifications];
299 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
300 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
301 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
302 [self addGenericPassword: @"data" account: account];
303 OCMVerifyAllWithDelay(self.mockDatabase, 20);
305 // Right now, the write in CloudKit is pending. Make the local modification...
306 [self updateGenericPassword: @"otherdata" account:account];
308 // And then schedule the update, but for the final version of the password
309 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
310 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"third"]];
312 // Stop the reencrypt operation from happening
313 self.keychainView.holdReencryptOutgoingItemsOperation = [CKKSGroupOperation named:@"reencrypt-hold" withBlock: ^{
314 ckksnotice_global("ckks", "releasing reencryption hold");
317 // The cloudkit operation finishes, letting the next OQO proceed (and set up the reencryption operation)
318 [self releaseCloudKitModificationHold];
320 // And wait for this to finish...
321 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
322 // And once more to quiesce.
323 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
325 [self updateGenericPassword: @"third" account:account];
327 // Item should upload.
328 OCMVerifyAllWithDelay(self.mockDatabase, 20);
330 // Run the reencrypt items operation to completion.
331 [self.operationQueue addOperation: self.keychainView.holdReencryptOutgoingItemsOperation];
332 [self.keychainView waitForOperationsOfClass:[CKKSReencryptOutgoingItemsOperation class]];
334 [self.keychainView waitUntilAllOperationsAreFinished];
335 [self waitForCKModifications];
338 - (void)testModifyItemDuringNetworkFailure {
339 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
340 NSString* account = @"account-delete-me";
342 [self startCKKSSubsystem];
343 [self holdCloudKitModifications];
345 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
346 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
348 [self addGenericPassword: @"data" account: account];
349 OCMVerifyAllWithDelay(self.mockDatabase, 20);
351 // Right now, the write in CloudKit is pending. Make the local modification...
352 [self updateGenericPassword: @"otherdata" account:account];
354 // And then schedule the update, but for the final version of the password
355 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
356 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
358 // The cloudkit operation finishes, letting the next OQO proceed (and set up uploading the new item)
359 [self releaseCloudKitModificationHold];
361 // Item should upload.
362 OCMVerifyAllWithDelay(self.mockDatabase, 20);
364 [self.keychainView waitUntilAllOperationsAreFinished];
365 [self waitForCKModifications];
368 - (void)testOutgoingQueueRecoverFromStaleInflightEntry {
369 // CKKS is restarting with an existing in-flight OQE
370 // Note that this test is incomplete, and doesn't re-add the item to the local keychain
371 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
372 NSString* account = @"fake-account";
374 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
375 NSError* error = nil;
377 CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
379 CKKSItem* item = [self newItem:ckrid withNewItemData:[self fakeRecordDictionary:account zoneID:self.keychainZoneID] key:self.keychainZoneKeys.classC];
380 XCTAssertNotNil(item, "Should be able to create a new fake item");
382 CKKSOutgoingQueueEntry* oqe = [[CKKSOutgoingQueueEntry alloc] initWithCKKSItem:item action:SecCKKSActionAdd state:SecCKKSStateInFlight waitUntil:nil accessGroup:@"ckks"];
383 XCTAssertNotNil(oqe, "Should be able to create a new fake OQE");
384 [oqe saveToDatabase:&error];
386 XCTAssertNil(error, "Shouldn't error saving new OQE to database");
387 return CKKSDatabaseTransactionCommit;
390 NSError *error = NULL;
391 XCTAssertEqual([CKKSOutgoingQueueEntry countByState:SecCKKSStateInFlight zone:self.keychainZoneID error:&error], 1,
392 "Expected on inflight entry in outgoing queue: %@", error);
394 // When CKKS restarts, it should find and re-upload this item
395 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
396 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
398 [self startCKKSSubsystem];
399 [self.keychainView waitForFetchAndIncomingQueueProcessing];
401 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
402 [self beginSOSTrustedViewOperation:self.keychainView];
403 [self.keychainView waitForKeyHierarchyReadiness];
404 OCMVerifyAllWithDelay(self.mockDatabase, 20);
407 - (void)testOutgoingQueueRecoverFromNetworkFailure {
408 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
409 NSString* account = @"account-delete-me";
411 [self startCKKSSubsystem];
412 [self holdCloudKitModifications];
414 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
416 NSError* greyMode = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNotAuthenticated userInfo:@{
417 CKErrorRetryAfterKey: @(0.2),
419 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:greyMode];
421 [self addGenericPassword: @"data" account: account];
422 OCMVerifyAllWithDelay(self.mockDatabase, 20);
424 // And then schedule the retried update
425 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
426 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
428 // The cloudkit operation finishes, letting the next OQO proceed (and set up uploading the new item)
429 [self releaseCloudKitModificationHold];
431 OCMVerifyAllWithDelay(self.mockDatabase, 20);
433 [self.keychainView waitUntilAllOperationsAreFinished];
434 [self waitForCKModifications];
437 - (void)testDeleteItem {
438 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
440 [self startCKKSSubsystem];
442 // We expect a single record to be uploaded.
443 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
444 [self addGenericPassword: @"data" account: @"account-delete-me"];
445 OCMVerifyAllWithDelay(self.mockDatabase, 20);
447 // We expect a single record to be deleted.
448 [self expectCKDeleteItemRecords: 1 zoneID:self.keychainZoneID];
449 [self deleteGenericPassword:@"account-delete-me"];
450 OCMVerifyAllWithDelay(self.mockDatabase, 20);
453 - (void)testDeleteItemAndReaddAtSameUUID {
454 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
456 [self startCKKSSubsystem];
458 // We expect a single record to be uploaded.
459 __block CKRecordID* itemRecordID = nil;
460 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:^BOOL(CKRecord * _Nonnull record) {
461 itemRecordID = record.recordID;
464 [self addGenericPassword:@"data" account:@"account-delete-me"];
465 OCMVerifyAllWithDelay(self.mockDatabase, 20);
467 // We expect a single record to be deleted.
468 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
469 [self deleteGenericPassword:@"account-delete-me"];
470 OCMVerifyAllWithDelay(self.mockDatabase, 20);
472 // And the item is readded. It should come back to its previous UUID.
473 XCTAssertNotNil(itemRecordID, "Should have an item record ID");
474 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:^BOOL(CKRecord * _Nonnull record) {
475 XCTAssertEqualObjects(itemRecordID.recordName, record.recordID.recordName, "Uploaded item UUID should match previous upload");
478 [self addGenericPassword:@"data" account:@"account-delete-me"];
479 OCMVerifyAllWithDelay(self.mockDatabase, 20);
482 - (void)testDeleteItemImmediatelyAfterModify {
483 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
484 NSString* account = @"account-delete-me";
486 [self startCKKSSubsystem];
488 // We expect a single record to be uploaded.
489 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
490 [self addGenericPassword: @"data" account: account];
491 OCMVerifyAllWithDelay(self.mockDatabase, 20);
493 // Now, hold the modify
494 [self holdCloudKitModifications];
496 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
497 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
498 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
500 [self updateGenericPassword: @"otherdata" account:account];
501 OCMVerifyAllWithDelay(self.mockDatabase, 20);
503 // Right now, the write in CloudKit is pending. Make the local deletion...
504 [self deleteGenericPassword:account];
506 // And then schedule the update
507 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
508 [self releaseCloudKitModificationHold];
510 OCMVerifyAllWithDelay(self.mockDatabase, 20);
513 - (void)testDeleteItemDuringAddUpload {
514 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
515 NSString* account = @"account-delete-me";
517 [self startCKKSSubsystem];
518 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"key state should enter 'ready'");
520 // We expect a single record to be uploaded. But, while that's happening, delete it via the API.
522 XCTestExpectation *deleteBlock = [self expectationWithDescription:@"delete block called"];
525 self.keychainZone.blockBeforeWriteOperation = ^() {
527 [self deleteGenericPassword:account];
528 self.keychainZone.blockBeforeWriteOperation = nil;
529 [deleteBlock fulfill];
532 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
534 // This should cause a deletion
535 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
536 [self addGenericPassword:@"data" account:account];
537 OCMVerifyAllWithDelay(self.mockDatabase, 20);
539 [self waitForExpectations: @[deleteBlock] timeout:5];
542 - (void)testDeleteItemDuringModificationUpload {
543 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
544 NSString* account = @"account-delete-me";
546 [self startCKKSSubsystem];
547 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"key state should enter 'ready'");
549 // We expect a single record to be uploaded.
550 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
551 [self addGenericPassword: @"data" account: account];
552 OCMVerifyAllWithDelay(self.mockDatabase, 20);
554 // We expect a single modification record to be uploaded, and want to delete the item while the upload is ongoing
555 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
556 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
557 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
559 XCTestExpectation *deleteBlock = [self expectationWithDescription:@"delete block called"];
562 self.keychainZone.blockBeforeWriteOperation = ^() {
564 [self deleteGenericPassword:account];
565 self.keychainZone.blockBeforeWriteOperation = nil;
566 [deleteBlock fulfill];
569 [self updateGenericPassword:@"otherdata" account:account];
570 OCMVerifyAllWithDelay(self.mockDatabase, 20);
572 [self waitForExpectations: @[deleteBlock] timeout:5];
575 - (void)testDeleteItemAfterFetchAfterModify {
576 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
577 NSString* account = @"account-delete-me";
579 [self startCKKSSubsystem];
581 // We expect a single record to be uploaded.
582 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
583 [self addGenericPassword: @"data" account: account];
584 OCMVerifyAllWithDelay(self.mockDatabase, 20);
586 // Now, hold the modify
587 //[self holdCloudKitModifications];
589 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
590 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
591 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
593 [self updateGenericPassword: @"otherdata" account:account];
594 OCMVerifyAllWithDelay(self.mockDatabase, 20);
596 // Right now, the write in CloudKit is pending. Place a hold on outgoing queue processing
597 // Place a hold on processing the outgoing queue.
598 self.keychainView.holdOutgoingQueueOperation = [CKKSResultOperation named:@"outgoing-queue-hold"
600 ckksnotice_global("ckks", "Outgoing queue hold released.");
603 [self deleteGenericPassword:account];
604 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
606 // Release the CK modification hold
607 //[self releaseCloudKitModificationHold];
610 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
611 [self.keychainView waitForFetchAndIncomingQueueProcessing];
612 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
614 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
615 OCMVerifyAllWithDelay(self.mockDatabase, 20);
618 - (void)testDeleteItemWithoutTombstones {
619 // The keychain API allows a client to ask for an inconsistent sync state:
620 // They can ask for a local item deletion without propagating the deletion off-device.
621 // This is the only halfway reasonable way to do keychain item deletions on account signout with the current API
623 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
624 NSString* account = @"account-delete-me";
626 [self startCKKSSubsystem];
627 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"key state should enter 'ready'");
628 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
630 // We expect a single record to be uploaded.
631 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
632 [self addGenericPassword: @"data" account: account];
633 OCMVerifyAllWithDelay(self.mockDatabase, 20);
635 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
637 [self deleteGenericPasswordWithoutTombstones:account];
638 [self findGenericPassword:account expecting:errSecItemNotFound];
640 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
642 // Ensure nothing is in the outgoing queue
643 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
644 NSError* error = nil;
645 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID
647 XCTAssertNil(error, "should be no error fetching uuids");
648 XCTAssertEqual(uuids.count, 0u, "There should be zero OQEs");
651 // And a simple fetch doesn't bring it back
652 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
653 [self.keychainView waitForFetchAndIncomingQueueProcessing];
654 [self findGenericPassword:account expecting:errSecItemNotFound];
657 CKKSSynchronizeOperation* resyncOperation = [self.keychainView resyncWithCloud];
658 [resyncOperation waitUntilFinished];
659 XCTAssertNil(resyncOperation.error, "No error during the resync operation");
662 [self findGenericPassword:account expecting:errSecSuccess];
664 OCMVerifyAllWithDelay(self.mockDatabase, 20);
668 - (void)testReceiveItem {
669 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
670 [self startCKKSSubsystem];
672 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
673 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
674 (id)kSecAttrAccount : @"account-delete-me",
675 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
676 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
679 CFTypeRef item = NULL;
680 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
682 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
684 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
685 [self.keychainZone addToZone: ckr];
687 // Trigger a notification (with hilariously fake data)
688 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
689 [self.keychainView waitForFetchAndIncomingQueueProcessing];
691 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
694 - (void)testReceiveManyItems {
695 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
696 [self startCKKSSubsystem];
698 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D00" withAccount:@"account0"]];
699 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D01" withAccount:@"account1"]];
700 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D02" withAccount:@"account2"]];
701 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D03" withAccount:@"account3"]];
702 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D04" withAccount:@"account4"]];
703 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D05" withAccount:@"account5"]];
704 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D06" withAccount:@"account6"]];
705 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D07" withAccount:@"account7"]];
706 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D08" withAccount:@"account8"]];
707 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D09" withAccount:@"account9"]];
708 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D10" withAccount:@"account10"]];
709 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D11" withAccount:@"account11"]];
711 for(int i = 12; i < 100; i++) {
713 NSString* recordName = [NSString stringWithFormat:@"7B598D31-F9C5-481E-98AC-%012d", i];
714 NSString* account = [NSString stringWithFormat:@"account%d", i];
716 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:recordName withAccount:account]];
720 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
721 [self.keychainView waitForFetchAndIncomingQueueProcessing];
723 [self findGenericPassword: @"account0" expecting:errSecSuccess];
724 [self findGenericPassword: @"account1" expecting:errSecSuccess];
725 [self findGenericPassword: @"account2" expecting:errSecSuccess];
726 [self findGenericPassword: @"account3" expecting:errSecSuccess];
727 [self findGenericPassword: @"account4" expecting:errSecSuccess];
728 [self findGenericPassword: @"account5" expecting:errSecSuccess];
729 [self findGenericPassword: @"account6" expecting:errSecSuccess];
730 [self findGenericPassword: @"account7" expecting:errSecSuccess];
731 [self findGenericPassword: @"account8" expecting:errSecSuccess];
732 [self findGenericPassword: @"account9" expecting:errSecSuccess];
733 [self findGenericPassword: @"account10" expecting:errSecSuccess];
734 [self findGenericPassword: @"account11" expecting:errSecSuccess];
737 - (void)testReceiveCollidingItem {
738 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
739 [self startCKKSSubsystem];
741 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
742 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
743 (id)kSecAttrAccount : @"account-delete-me",
744 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
745 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
748 CFTypeRef item = NULL;
749 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
751 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
752 CKRecord* ckr2 = [self createFakeRecord: self.keychainZoneID recordName: @"F9C58D31-7B59-481E-98AC-5A507ACB2D85"];
754 [self.keychainZone addToZone: ckr];
755 [self.keychainZone addToZone: ckr2];
757 // We expect a delete operation with the "higher" UUID.
758 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
760 // Trigger a notification (with hilariously fake data)
761 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
763 OCMVerifyAllWithDelay(self.mockDatabase, 20);
764 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
766 [self waitForCKModifications];
767 XCTAssertNil(self.keychainZone.currentDatabase[ckr2.recordID], "Correct record was deleted from CloudKit");
769 // And the local item should have ckr's UUID
770 [self checkGenericPasswordStoredUUID:ckr.recordID.recordName account:@"account-delete-me"];
773 - (void)testReceiveCorruptedItem {
774 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
775 [self startCKKSSubsystem];
777 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
779 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
781 // I don't know of any codepaths that cause this, but it apparently has happened.
782 ckr[SecCKRecordWrappedKeyKey] = nil;
783 [self.keychainZone addToZone:ckr];
785 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
786 [self.keychainView waitForFetchAndIncomingQueueProcessing];
788 // The item still shouldn't exist, because it was corrupted in flight
789 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
791 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
792 NSError* error = nil;
793 NSArray<CKKSIncomingQueueEntry*>* iqes = [CKKSIncomingQueueEntry all:&error];
794 XCTAssertNil(error, "No error loading IQEs");
795 XCTAssertNotNil(iqes, "Could load IQEs");
796 XCTAssertEqual(iqes.count, 1u, "Incoming queue has one item");
797 XCTAssertEqualObjects(iqes[0].state, SecCKKSStateNew, "Item state should be 'new'");
801 -(void)testReceiveItemDelete {
802 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
803 [self startCKKSSubsystem];
805 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
806 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
807 (id)kSecAttrAccount : @"account-delete-me",
808 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
809 (id)kSecReturnAttributes : @YES,
810 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
813 CFTypeRef cfitem = NULL;
814 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfitem), "item should not yet exist");
816 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
817 [self.keychainView waitForFetchAndIncomingQueueProcessing];
819 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
820 [self.keychainZone addToZone: ckr];
822 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
823 [self.keychainView waitForFetchAndIncomingQueueProcessing];
825 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfitem), "item should exist now");
827 NSDictionary* item = (NSDictionary*) CFBridgingRelease(cfitem);
829 NSDate* itemModificationDate = item[(id)kSecAttrModificationDate];
830 XCTAssertNotNil(itemModificationDate, "Should have a modification date");
833 [self.keychainZone deleteCKRecordIDFromZone: [ckr recordID]];
834 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
835 [self.keychainView waitForFetchAndIncomingQueueProcessing];
837 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfitem), "item should no longer exist");
838 CFReleaseNull(cfitem);
840 // Now, double-check the tombstone. Its modification date should be derived from the item's mdat.
841 NSDictionary *tombquery = @{(id)kSecClass : (id)kSecClassGenericPassword,
842 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
843 (id)kSecAttrAccount : @"account-delete-me",
844 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
845 (id)kSecAttrTombstone : @YES,
846 (id)kSecReturnAttributes : @YES,
847 (id)kSecMatchLimit : (id)kSecMatchLimitOne,};
849 CFTypeRef cfref = NULL;
850 OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)tombquery, &cfref);
851 XCTAssertEqual(status, errSecSuccess, "Should have found a tombstone");
853 NSDictionary* tombstone = (NSDictionary*)CFBridgingRelease(cfref);
854 XCTAssertNotNil(tombstone, "Should have found a tombstone");
856 NSDate* tombstoneModificationDate = tombstone[(id)kSecAttrModificationDate];
857 XCTAssertEqual([tombstoneModificationDate compare:itemModificationDate], NSOrderedDescending, "tombstone should be later than item");
859 NSTimeInterval tombestoneDelta = [tombstoneModificationDate timeIntervalSinceDate:itemModificationDate];
860 XCTAssertGreaterThan(tombestoneDelta, 0, "Delta should be positive");
861 XCTAssertLessThan(tombestoneDelta, 5, "tombstone mdat should be no later than 5s after item mdat");
863 // And just as a sanity, mdat is already far ago, right?
864 NSTimeInterval itemDelta = [[NSDate date] timeIntervalSinceDate:itemModificationDate];
865 XCTAssertGreaterThan(itemDelta, 10, "item mdat should at least 10s in the past");
868 - (void)testReceiveTombstoneItem {
869 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
870 [self startCKKSSubsystem];
872 NSString* account = @"account-delete-me";
874 CKRecord* ckr = [self createFakeTombstoneRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" account:account];
875 [self.keychainZone addToZone:ckr];
877 // This device should delete the tombstone entry
878 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
880 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
881 [self.keychainView waitForFetchAndIncomingQueueProcessing];
883 // The tombstone shouldn't exist
884 NSDictionary *tombquery = @{
885 (id)kSecClass : (id)kSecClassGenericPassword,
886 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
887 (id)kSecAttrAccount : account,
888 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
889 (id)kSecReturnAttributes : @YES,
890 (id)kSecAttrTombstone : @YES,
893 CFTypeRef cftype = NULL;
894 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef)tombquery, &cftype), "item should not exist now");
895 XCTAssertNil((__bridge id)cftype, "Should have found no tombstones");
897 // And the delete should occur
898 OCMVerifyAllWithDelay(self.mockDatabase, 20);
900 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
901 NSError* error = nil;
902 NSArray<NSString*>* uuids = [CKKSIncomingQueueEntry allUUIDs:self.keychainZoneID
904 XCTAssertNil(error, "should be no error fetching uuids");
905 XCTAssertEqual(uuids.count, 0u, "There should be zero IQEs");
909 - (void)testReceiveItemDeleteAndReaddAtDifferentUUIDInSameFetch {
910 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
911 [self startCKKSSubsystem];
913 NSString* itemAccount = @"account-delete-me";
914 [self findGenericPassword:itemAccount expecting:errSecItemNotFound];
916 NSString* uuidOriginalItem = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
917 NSString* uuidGreater = @"7B598D31-FFFF-FFFF-98AC-5A507ACB2D85";
919 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:uuidOriginalItem];
920 CKRecord* ckrGreater = [self createFakeRecord:self.keychainZoneID recordName:uuidGreater];
922 [self.keychainZone addToZone:ckr];
924 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
925 [self.keychainView waitForFetchAndIncomingQueueProcessing];
927 [self findGenericPassword:itemAccount expecting:errSecSuccess];
928 [self checkGenericPasswordStoredUUID:uuidOriginalItem account:itemAccount];
930 // Now, the item is deleted and re-added with a greater UUID
931 [self.keychainZone deleteCKRecordIDFromZone:[ckr recordID]];
932 [self.keychainZone addToZone:ckrGreater];
934 // This node should not upload anything.
935 [[self.mockDatabase reject] addOperation:[OCMArg any]];
937 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
938 [self.keychainView waitForFetchAndIncomingQueueProcessing];
940 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
941 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
943 // Item should still exist.
944 [self findGenericPassword:itemAccount expecting:errSecSuccess];
945 [self checkGenericPasswordStoredUUID:uuidGreater account:itemAccount];
948 -(void)testReceiveItemPhantomDelete {
949 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
950 [self startCKKSSubsystem];
952 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
953 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
954 (id)kSecAttrAccount : @"account-delete-me",
955 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
956 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
959 CFTypeRef item = NULL;
960 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
962 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
963 [self.keychainView waitForFetchAndIncomingQueueProcessing];
965 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
966 [self.keychainZone addToZone: ckr];
968 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
969 [self.keychainView waitForFetchAndIncomingQueueProcessing];
971 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
974 [self.keychainView waitUntilAllOperationsAreFinished];
977 [self.keychainZone deleteCKRecordIDFromZone: [ckr recordID]];
979 // and add another, incorrect IQE
980 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
981 // Inefficient, but hey, it works
982 CKRecord* record = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-FFFF-FFFF-5A507ACB2D85"];
983 CKKSItem* fakeItem = [[CKKSItem alloc] initWithCKRecord: record];
985 CKKSIncomingQueueEntry* iqe = [[CKKSIncomingQueueEntry alloc] initWithCKKSItem:fakeItem
986 action:SecCKKSActionDelete
987 state:SecCKKSStateNew];
988 XCTAssertNotNil(iqe, "could create fake IQE");
989 NSError* error = nil;
990 XCTAssert([iqe saveToDatabase: &error], "Saved fake IQE to database");
991 XCTAssertNil(error, "No error saving fake IQE to database");
992 return CKKSDatabaseTransactionCommit;
995 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
996 [self.keychainView waitForFetchAndIncomingQueueProcessing];
998 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
1000 // The incoming queue should be empty
1001 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
1002 NSError* error = nil;
1003 NSArray* iqes = [CKKSIncomingQueueEntry all:&error];
1004 XCTAssertNil(error, "No error loading IQEs");
1005 XCTAssertNotNil(iqes, "Could load IQEs");
1006 XCTAssertEqual(iqes.count, 0u, "Incoming queue is empty");
1010 -(void)testReceiveConflictOnJustAddedItem {
1011 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1012 [self startCKKSSubsystem];
1014 [self.keychainView waitForKeyHierarchyReadiness];
1015 [self.keychainView waitUntilAllOperationsAreFinished];
1017 // Place a hold on processing the outgoing queue.
1018 self.keychainView.holdOutgoingQueueOperation = [CKKSResultOperation named:@"outgoing-queue-hold"
1020 ckksnotice_global("ckks", "Outgoing queue hold released.");
1023 self.keychainView.holdIncomingQueueOperation = [CKKSResultOperation named:@"incoming-queue-hold"
1025 ckksnotice_global("ckks", "Incoming queue hold released.");
1028 [self addGenericPassword:@"localchange" account:@"account-delete-me"];
1030 // Pull out the new item's UUID.
1031 __block NSString* itemUUID = nil;
1032 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
1033 NSError* error = nil;
1034 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID ?: [[CKRecordZoneID alloc] initWithZoneName:@"keychain"
1035 ownerName:CKCurrentUserDefaultName]
1037 XCTAssertNil(error, "no error fetching uuids");
1038 XCTAssertEqual(uuids.count, 1u, "There's exactly one outgoing queue entry");
1039 itemUUID = uuids[0];
1041 XCTAssertNotNil(itemUUID, "Have a UUID for our new item");
1044 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName: itemUUID]];
1046 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
1047 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
1049 // Allow the outgoing queue operation to proceed
1050 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
1051 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
1053 // Allow the incoming queue operation to proceed
1054 [self.operationQueue addOperation:self.keychainView.holdIncomingQueueOperation];
1055 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
1057 [self checkGenericPassword:@"data" account:@"account-delete-me"];
1059 [self.keychainView waitUntilAllOperationsAreFinished];
1062 - (void)testReceiveCloudKitConflictOnJustAddedItems {
1063 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1064 [self startCKKSSubsystem];
1066 [self.keychainView waitForKeyHierarchyReadiness];
1067 [self.keychainView waitUntilAllOperationsAreFinished];
1069 // Place a hold on processing the outgoing queue.
1070 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
1071 self.keychainView.holdOutgoingQueueOperation = [CKKSResultOperation named:@"outgoing-queue-hold" withBlock:^{
1072 ckksnotice_global("ckks", "Outgoing queue hold released.");
1075 [self addGenericPassword:@"localchange" account:@"account-delete-me"];
1077 // Pull out the new item's UUID.
1078 __block NSString* itemUUID = nil;
1079 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
1080 NSError* error = nil;
1081 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID ?: [[CKRecordZoneID alloc] initWithZoneName:@"keychain"
1082 ownerName:CKCurrentUserDefaultName]
1084 XCTAssertNil(error, "no error fetching uuids");
1085 XCTAssertEqual(uuids.count, 1u, "There's exactly one outgoing queue entry");
1086 itemUUID = uuids[0];
1088 XCTAssertNotNil(itemUUID, "Have a UUID for our new item");
1091 // Add a second item: this item should be uploaded after the failure of the first item
1092 [self addGenericPassword:@"localchange" account:@"account-delete-me-2"];
1094 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName: itemUUID]];
1096 // Also, this write will increment the class C current pointer's etag
1097 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
1098 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
1099 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
1100 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
1101 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
1103 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
1104 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1105 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1107 // Allow the outgoing queue operation to proceed
1108 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
1110 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1111 [self.keychainView waitUntilAllOperationsAreFinished];
1113 [self checkGenericPassword:@"data" account:@"account-delete-me"];
1114 [self checkGenericPassword:@"localchange" account:@"account-delete-me-2"];
1118 -(void)testReceiveUnknownField {
1119 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1121 [self startCKKSSubsystem];
1122 [self.keychainView waitForKeyHierarchyReadiness];
1124 NSError* error = nil;
1126 // Manually encrypt an item
1127 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
1128 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
1129 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
1130 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
1131 parentKeyUUID:self.keychainZoneKeys.classA.uuid
1132 zoneID:recordID.zoneID];
1133 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classA error:&error];
1134 XCTAssertNotNil(itemkey, "Got a key");
1135 cipheritem.wrappedkey = itemkey.wrappedkey;
1136 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
1138 NSData* future_data_field = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding];
1139 NSString* future_string_field = @"authstring";
1140 NSString* future_server_field = @"server_can_change_at_any_time";
1141 NSNumber* future_number_field = [NSNumber numberWithInt:30];
1143 // Use version 2, so future fields will be authenticated
1144 cipheritem.encver = CKKSItemEncryptionVersion2;
1145 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
1147 authenticatedData[@"future_data_field"] = future_data_field;
1148 authenticatedData[@"future_string_field"] = [future_string_field dataUsingEncoding:NSUTF8StringEncoding];
1150 uint64_t n = OSSwapHostToLittleConstInt64([future_number_field unsignedLongValue]);
1151 authenticatedData[@"future_number_field"] = [NSData dataWithBytes:&n length:sizeof(n)];
1154 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
1155 XCTAssertNil(error, "no error encrypting object");
1156 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
1158 CKRecord* ckr = [cipheritem CKRecordWithZoneID: recordID.zoneID];
1159 ckr[@"future_data_field"] = future_data_field;
1160 ckr[@"future_string_field"] = future_string_field;
1161 ckr[@"future_number_field"] = future_number_field;
1162 ckr[@"server_new_server_field"] = future_server_field;
1163 [self.keychainZone addToZone:ckr];
1165 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
1166 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1168 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
1169 (id)kSecReturnAttributes: @YES,
1170 (id)kSecAttrSynchronizable: @YES,
1171 (id)kSecAttrAccount: @"account-delete-me",
1172 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
1174 CFTypeRef cfresult = NULL;
1175 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
1177 // Test that if this item is updated, it remains encrypted in v2, and future_field still exists
1178 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1179 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
1181 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1182 [self waitForCKModifications];
1184 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
1185 XCTAssertEqualObjects(newRecord[@"future_data_field"], future_data_field, "future_data_field still exists");
1186 XCTAssertEqualObjects(newRecord[@"future_string_field"], future_string_field, "future_string_field still exists");
1187 XCTAssertEqualObjects(newRecord[@"future_number_field"], future_number_field, "future_string_field still exists");
1188 XCTAssertEqualObjects(newRecord[@"server_new_server_field"], future_server_field, "future_server_field stille exists");
1190 CKKSItem* newItem = [[CKKSItem alloc] initWithCKRecord:newRecord];
1191 CKKSAESSIVKey* newItemKey = [self.keychainZoneKeys.classA unwrapAESKey:newItem.wrappedkey error:&error];
1192 XCTAssertNil(error, "No error unwrapping AES key");
1193 XCTAssertNotNil(newItemKey, "Have an unwrapped AES key for this item");
1195 NSDictionary* uploadedData = [CKKSItemEncrypter decryptDictionary:newRecord[SecCKRecordDataKey]
1197 authenticatedData:authenticatedData
1199 XCTAssertNil(error, "No error decrypting dictionary");
1200 XCTAssertNotNil(uploadedData, "Authenticated re-uploaded data including future_field");
1201 XCTAssertEqualObjects(uploadedData[@"v_Data"], [@"different password" dataUsingEncoding:NSUTF8StringEncoding], "Passwords match");
1205 -(void)testReceiveRecordEncryptedv1 {
1206 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1208 [self startCKKSSubsystem];
1209 [self.keychainView waitForKeyHierarchyReadiness];
1211 NSError* error = nil;
1213 // Manually encrypt an item
1214 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
1215 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
1216 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
1217 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
1218 parentKeyUUID:self.keychainZoneKeys.classC.uuid
1219 zoneID:recordID.zoneID];
1220 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
1221 XCTAssertNotNil(itemkey, "Got a key");
1222 cipheritem.wrappedkey = itemkey.wrappedkey;
1223 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
1225 cipheritem.encver = CKKSItemEncryptionVersion1;
1227 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:cipheritem.encver] mutableCopy];
1229 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
1230 XCTAssertNil(error, "no error encrypting object");
1231 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
1233 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
1235 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
1236 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1238 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
1239 (id)kSecReturnAttributes: @YES,
1240 (id)kSecAttrSynchronizable: @YES,
1241 (id)kSecAttrAccount: @"account-delete-me",
1242 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
1244 CFTypeRef cfresult = NULL;
1245 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
1246 CFReleaseNull(cfresult);
1248 // Test that if this item is updated, it is encrypted in v2
1249 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1250 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
1252 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1253 [self waitForCKModifications];
1255 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
1256 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
1259 - (void)testLocalUpdateToTombstoneItem {
1260 // Some CKKS clients may accidentally upload entries with tomb=1.
1261 // We should delete these items with extreme predjudice.
1262 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
1264 [self startCKKSSubsystem];
1266 // We expect a single record to be uploaded.
1267 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
1268 [self addGenericPassword: @"data" account: @"account-delete-me"];
1269 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1271 // We expect a single record to be deleted.
1272 [self expectCKDeleteItemRecords: 1 zoneID:self.keychainZoneID];
1273 [self deleteGenericPassword:@"account-delete-me"];
1274 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1276 // Now, SOS comes along and updates the tombstone
1277 // CKKS should _not_ try to upload a tombstone
1278 NSDictionary *tombquery = @{
1279 (id)kSecClass : (id)kSecClassGenericPassword,
1280 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1281 (id)kSecAttrAccount : @"account-delete-me",
1282 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1283 (id)kSecAttrTombstone : @YES,
1286 NSDictionary* update = @{
1287 (id)kSecAttrModificationDate : [NSDate date],
1290 __block CFErrorRef cferror = NULL;
1291 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
1292 bool ok = kc_transaction_type(dbt, kSecDbExclusiveRemoteSOSTransactionType, &cferror, ^bool {
1293 OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)tombquery, (__bridge CFDictionaryRef)update);
1294 XCTAssertEqual(status, errSecSuccess, "Should have been able to update a tombstone");
1301 XCTAssertNil((__bridge NSError*)cferror, "Should be no error updating a tombstone");
1303 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
1306 - (void)testIgnoreUpdateToModificationDateItem {
1307 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
1308 [self startCKKSSubsystem];
1310 // We expect a single record to be uploaded.
1311 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
1312 [self addGenericPassword:@"data" account: @"account-delete-me"];
1313 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1315 // Nothing more should be uploaded
1316 NSDictionary *query = @{
1317 (id)kSecClass : (id)kSecClassGenericPassword,
1318 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1319 (id)kSecAttrAccount : @"account-delete-me",
1320 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1323 NSDictionary* update = @{
1324 (id)kSecAttrModificationDate : [NSDate date],
1327 __block CFErrorRef cferror = NULL;
1328 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
1329 bool ok = kc_transaction_type(dbt, kSecDbExclusiveRemoteSOSTransactionType, &cferror, ^bool {
1330 OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update);
1331 XCTAssertEqual(status, errSecSuccess, "Should have been able to update the item");
1338 XCTAssertNil((__bridge NSError*)cferror, "Should be no error updating just the mdat");
1340 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
1343 - (void)testUploadPagination {
1344 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1345 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1346 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1347 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1349 for(size_t count = 0; count < 250; count++) {
1350 [self addGenericPassword: @"data" account: [NSString stringWithFormat:@"account-delete-me-%03lu", count]];
1353 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1354 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1355 [self expectCKModifyItemRecords: 50 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1357 [self startCKKSSubsystem];
1359 // For the next 5 seconds, try to add and then find an item. Each attempt should be fairly quick: no long multisecond pauses while CKKS Scans
1360 NSTimeInterval elapsed = 0;
1362 while(elapsed < 10) {
1363 NSDate* begin = [NSDate now];
1365 NSString* account = [NSString stringWithFormat:@"non-syncable-%d", (int)count];
1367 NSDictionary* query = @{
1368 (id)kSecClass : (id)kSecClassGenericPassword,
1369 (id)kSecAttrAccount : account,
1370 (id)kSecAttrSynchronizable : @NO,
1371 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1372 (id)kSecValueData : [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
1375 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)query, NULL), errSecSuccess, @"Should be able to add nonsyncable item");
1376 ckksnotice("ckkstest", self.keychainView, "SecItemAdd of %@ successful", account);
1378 NSDictionary *findQuery = @{
1379 (id)kSecClass : (id)kSecClassGenericPassword,
1380 (id)kSecAttrAccount : account,
1381 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1382 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1384 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, NULL), errSecSuccess, "Finding item %@", account);
1385 ckksnotice("ckkstest", self.keychainView, "SecItemCopyMatching of %@ successful", account);
1387 NSDate* end = [NSDate now];
1388 NSTimeInterval delta = [end timeIntervalSinceDate:begin];
1390 XCTAssertLessThan(delta, 2, @"Keychain API should respond in two seconds");
1391 ckksnotice("ckkstest", self.keychainView, "SecItemAdd/SecItemCopyMatching pair of %@ took %.4fs", account, delta);
1393 usleep(10000); // sleep for 10ms, to let some other things get done
1395 // And retake the time elasped for the overall count
1396 elapsed += [[NSDate now] timeIntervalSinceDate:begin];
1400 OCMVerifyAllWithDelay(self.mockDatabase, 40);
1403 - (void)testUploadInitialKeyHierarchy {
1404 // Test starts with nothing in database. CKKS should get into the "please upload my keys" state, then Octagon should perform the upload
1406 // Spin up CKKS subsystem.
1407 [self startCKKSSubsystem];
1409 [self performOctagonTLKUpload:self.ckksViews];
1410 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1412 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
1415 - (void)testDoNotErrorIfNudgedWhileWaitingForTLKUpload {
1416 // Test starts with nothing in database. CKKS should get into the "please upload my keys" state, then Octagon should perform the upload
1418 // Spin up CKKS subsystem.
1419 [self startCKKSSubsystem];
1421 NSMutableArray<CKKSResultOperation<CKKSKeySetProviderOperationProtocol>*>* keysetOps = [NSMutableArray array];
1423 for(CKKSKeychainView* view in self.ckksViews) {
1424 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:40*NSEC_PER_SEC], @"key state should enter 'waitfortlkcreation' (view %@)", view);
1425 [keysetOps addObject: [view findKeySet:NO]];
1428 // Now that we've kicked them all off, wait for them to resolve (and nudge each one, as if a key was saved)
1429 for(CKKSKeychainView* view in self.ckksViews) {
1430 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKUpload] wait:40*NSEC_PER_SEC], @"key state should enter 'waitfortlkupload'");
1432 CKKSCondition* viewProcess = view.keyHierarchyConditions[SecCKKSZoneKeyStateProcess];
1433 [view keyStateMachineRequestProcess];
1435 // Since we do need to leave SecCKKSZoneKeyStateWaitForTLKUpload if a fetch occurs with new keys, make sure we do the right thing
1436 XCTAssertEqual(0, [viewProcess wait:10*NSEC_PER_MSEC], "CKKS should reprocess the key hierarchy when nudged");
1437 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKUpload] wait:40*NSEC_PER_SEC], @"key state should re-enter 'waitfortlkupload'");
1440 // The views should remain in waitfortlkcreation, and not go through process into an error
1442 NSMutableArray<CKRecord*>* keyHierarchyRecords = [NSMutableArray array];
1444 for(CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp in keysetOps) {
1445 // Wait until finished is usually a bad idea. We could rip this out into an operation if we'd like.
1446 [keysetOp waitUntilFinished];
1447 XCTAssertNil(keysetOp.error, "Should be no error fetching keyset from CKKS");
1449 NSArray<CKRecord*>* records = [self putKeySetInCloudKit:keysetOp.keyset];
1450 [keyHierarchyRecords addObjectsFromArray:records];
1453 // Tell our views about our shiny new records!
1454 for(CKKSKeychainView* view in self.ckksViews) {
1455 [view receiveTLKUploadRecords: keyHierarchyRecords];
1457 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1459 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
1463 - (void)testReceiveChangedKeySetFromWaitingForTLKUpload {
1464 // Test starts with nothing in database. CKKS should get into the "please upload my keys" state
1466 [self startCKKSSubsystem];
1468 // After each zone arrives in WaitForTLKCreation, new keys are uploaded
1469 for(CKKSKeychainView* view in self.ckksViews) {
1470 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:40*NSEC_PER_SEC], @"key state should enter 'waitfortlkcreation' (view %@)", view);
1473 for(CKKSKeychainView* view in self.ckksViews) {
1474 [self putFakeKeyHierarchyInCloudKit:view.zoneID];
1475 [self putFakeDeviceStatusInCloudKit:view.zoneID];
1478 // If we ask the zones for their keysets, they should return the local set ready for upload
1479 NSMutableArray<CKKSResultOperation<CKKSKeySetProviderOperationProtocol>*>* keysetOps = [NSMutableArray array];
1481 for(CKKSKeychainView* view in self.ckksViews) {
1482 [keysetOps addObject:[view findKeySet:NO]];
1485 for(CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp in keysetOps) {
1486 [keysetOp waitUntilFinished];
1487 XCTAssertNil(keysetOp.error, "Should be no error fetching keyset from CKKS");
1489 CKRecordZoneID* zoneID = [[CKRecordZoneID alloc] initWithZoneName:keysetOp.zoneName
1490 ownerName:CKCurrentUserDefaultName];
1491 ZoneKeys* zk = self.keys[zoneID];
1492 XCTAssertNotNil(zk, "Should have new zone keys for zone %@", keysetOp.zoneName);
1493 XCTAssertNotEqualObjects(keysetOp.keyset.currentTLKPointer.currentKeyUUID, zk.tlk.uuid, "Fetched TLK and CK TLK should be different");
1496 // Now, find the keysets again, asking for a fetch this time
1497 NSMutableArray<CKKSResultOperation<CKKSKeySetProviderOperationProtocol>*>* fetchedKeysetOps = [NSMutableArray array];
1499 for(CKKSKeychainView* view in self.ckksViews) {
1500 [fetchedKeysetOps addObject:[view findKeySet:YES]];
1503 for(CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp in fetchedKeysetOps) {
1504 [keysetOp waitUntilFinished];
1505 XCTAssertNil(keysetOp.error, "Should be no error fetching keyset from CKKS");
1507 CKRecordZoneID* zoneID = [[CKRecordZoneID alloc] initWithZoneName:keysetOp.zoneName
1508 ownerName:CKCurrentUserDefaultName];
1509 ZoneKeys* zk = self.keys[zoneID];
1510 XCTAssertNotNil(zk, "Should have new zone keys for zone %@", keysetOp.zoneName);
1511 XCTAssertEqualObjects(keysetOp.keyset.currentTLKPointer.currentKeyUUID, zk.tlk.uuid, "Fetched TLK and CK TLK should now match");
1515 - (void)testProvideKeysetFromNoTrust {
1516 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1518 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1519 [self startCKKSSubsystem];
1521 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortrust'");
1523 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet:NO];
1524 [keysetOp timeout:20*NSEC_PER_SEC];
1525 [keysetOp waitUntilFinished];
1527 XCTAssertNil(keysetOp.error, "Should be no error fetching a keyset");
1530 - (void)testProvideKeysetFromNoTrustWithRefetch {
1531 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1533 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1534 [self startCKKSSubsystem];
1536 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortrust'");
1538 self.silentFetchesAllowed = false;
1539 [self expectCKFetch];
1541 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet:YES];
1542 [keysetOp timeout:20*NSEC_PER_SEC];
1543 [keysetOp waitUntilFinished];
1545 XCTAssertNil(keysetOp.error, "Should be no error fetching a keyset");
1547 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1550 - (void)testProvideKeysetAfterReceivingTLKInNoTrust {
1551 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1553 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1554 [self startCKKSSubsystem];
1556 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortrust'");
1558 // This isn't necessarily SOS, but perhaps SBD.
1559 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1561 // Still ends up in waitfortrust...
1562 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortrust'");
1564 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet:NO];
1565 [keysetOp timeout:20*NSEC_PER_SEC];
1566 [keysetOp waitUntilFinished];
1568 XCTAssertNil(keysetOp.error, "Should be no error fetching a keyset");
1569 XCTAssertNotNil(keysetOp.keyset, "Should have a keyset");
1570 XCTAssertNotNil(keysetOp.keyset.tlk, "Should have a TLK");
1573 - (void)testUploadInitialKeyHierarchyAfterLockedStart {
1574 // 'Lock' the keybag
1575 self.aksLockState = true;
1576 [self.lockStateTracker recheck];
1578 [self startCKKSSubsystem];
1580 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitfortlkcreation");
1582 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet:NO];
1584 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1585 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitforunlock");
1587 // After unlock, the key hierarchy should be created.
1588 self.aksLockState = false;
1589 [self.lockStateTracker recheck];
1591 [keysetOp timeout:10 * NSEC_PER_SEC];
1592 [keysetOp waitUntilFinished];
1593 XCTAssertNil(keysetOp.error, @"Should be no error performing keyset op");
1595 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKUpload] wait:20*NSEC_PER_SEC], @"Key state should enter 'waitfortlkupload'");
1597 NSArray<CKRecord*>* keyHierarchyRecords = [self putKeySetInCloudKit:keysetOp.keyset];
1598 [self.keychainView receiveTLKUploadRecords:keyHierarchyRecords];
1600 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should enter 'ready'");
1602 // We expect a single class C record to be uploaded.
1603 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1605 [self addGenericPassword: @"data" account: @"account-delete-me"];
1606 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1609 - (void)testExitWaitForTLKUploadIfTLKsCreated {
1610 [self startCKKSSubsystem];
1612 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitfortlkcreation");
1614 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet:NO];
1616 [keysetOp timeout:10 * NSEC_PER_SEC];
1617 [keysetOp waitUntilFinished];
1618 XCTAssertNil(keysetOp.error, @"Should be no error performing keyset op");
1620 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKUpload] wait:20*NSEC_PER_SEC], @"Key state should enter 'waitfortlkupload'");
1622 // But another device beats us to it!
1623 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1624 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1626 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
1628 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should enter 'waitfortlk'");
1629 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1632 - (void)testExitWaitForTLKUploadIfTLKsCreatedWhileNoTrust {
1633 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1634 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1636 [self startCKKSSubsystem];
1638 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitfortlkcreation");
1640 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet:NO];
1642 [keysetOp timeout:10 * NSEC_PER_SEC];
1643 [keysetOp waitUntilFinished];
1644 XCTAssertNil(keysetOp.error, @"Should be no error performing keyset op");
1646 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKUpload] wait:20*NSEC_PER_SEC], @"Key state should enter 'waitfortlkupload'");
1648 // But another device beats us to it!
1649 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1650 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1652 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
1654 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], @"Key state should enter 'waitfortrust'");
1655 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1658 - (void)testLockImmediatelyAfterUploadingInitialKeyHierarchy {
1660 __weak __typeof(self) weakSelf = self;
1662 [self startCKKSSubsystem];
1663 [self performOctagonTLKUpload:self.ckksViews afterUpload:^{
1664 __strong __typeof(self) strongSelf = weakSelf;
1665 [strongSelf holdCloudKitFetches];
1668 // Should enter 'ready'
1669 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1670 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1672 // Now, lock and allow fetches again
1673 self.aksLockState = true;
1674 [self.lockStateTracker recheck];
1675 [self releaseCloudKitFetchHold];
1677 CKKSResultOperation* op = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
1678 [op waitUntilFinished];
1680 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1682 // Wait for CKKS to shake itself out...
1683 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
1685 // Should be in ReadyPendingUnlock
1686 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1688 // We expect a single class C record to be uploaded.
1689 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1690 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1692 [self addGenericPassword: @"data" account: @"account-delete-me"];
1693 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1696 - (void)testReceiveKeyHierarchyAfterLockedStart {
1697 // 'Lock' the keybag
1698 self.aksLockState = true;
1699 [self.lockStateTracker recheck];
1701 [self startCKKSSubsystem];
1703 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1704 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitfortlkcreation");
1706 // Now, another device comes along and creates the hierarchy; we download it; and it and sends us the TLK
1707 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1708 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1709 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
1710 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
1712 self.aksLockState = false;
1713 [self.lockStateTracker recheck];
1714 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should end up in waitfortlk");
1716 // After unlock, the TLK arrives
1717 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1718 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1720 // We expect a single class C record to be uploaded.
1721 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1723 [self addGenericPassword: @"data" account: @"account-delete-me"];
1724 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1725 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1728 - (void)testLoadKeyHierarchyAfterLockedStart {
1729 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
1731 // 'Lock' the keybag
1732 self.aksLockState = true;
1733 [self.lockStateTracker recheck];
1735 [self startCKKSSubsystem];
1737 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1738 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
1740 self.aksLockState = false;
1741 [self.lockStateTracker recheck];
1743 // We expect a single class C record to be uploaded.
1744 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1746 [self addGenericPassword: @"data" account: @"account-delete-me"];
1747 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1750 - (void)testUploadAndUseKeyHierarchy {
1751 [self startCKKSSubsystem];
1752 [self performOctagonTLKUpload:self.ckksViews];
1754 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
1755 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1756 (id)kSecAttrAccount : @"account-delete-me",
1757 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1758 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
1760 CFTypeRef item = NULL;
1761 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not exist");
1763 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1764 [self waitForCKModifications];
1766 // We expect a single class C record to be uploaded.
1767 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1769 [self addGenericPassword: @"data" account: @"account-delete-me"];
1770 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1772 // now, expect a single class A record to be uploaded
1773 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1775 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef)@{
1776 (id)kSecClass : (id)kSecClassGenericPassword,
1777 (id)kSecAttrAccessGroup : @"com.apple.security.sos",
1778 (id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked,
1779 (id)kSecAttrAccount : @"account-class-A",
1780 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1781 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
1782 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
1783 }, NULL), @"Adding class A item");
1784 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1787 - (void)testUploadInitialKeyHierarchyTriggersBackup {
1788 // We also expect the view manager's notifyNewTLKsInKeychain call to fire (after some delay)
1789 OCMExpect([self.mockCKKSViewManager notifyNewTLKsInKeychain]);
1791 // Spin up CKKS subsystem.
1792 [self startCKKSSubsystem];
1793 [self performOctagonTLKUpload:self.ckksViews];
1795 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1796 OCMVerifyAllWithDelay(self.mockCKKSViewManager, 10);
1799 - (void)testResetCloudKitZoneFromNoTLK {
1800 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1801 OCMExpect([self.suggestTLKUpload trigger]);
1803 self.silentZoneDeletesAllowed = true;
1805 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1806 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1807 // explicitly do not save a fake device status here
1808 self.keychainZone.flag = true;
1810 [self startCKKSSubsystem];
1811 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1813 // But then, it'll fire off the reset and reach 'ready', with a little help from octagon
1814 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1815 [self performOctagonTLKUpload:self.ckksViews];
1817 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1818 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1820 // And the zone should have been cleared and re-made
1821 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1824 - (void)testResetCloudKitZoneFromNoTLKWithOtherWaitForTLKDevices {
1825 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1826 OCMExpect([self.suggestTLKUpload trigger]);
1828 self.silentZoneDeletesAllowed = true;
1830 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1831 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1832 // Save a fake device status here, but modify its key state to be 'waitfortlk': it has no idea what the TLK is either
1833 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1834 [self putFakeOctagonOnlyDeviceStatusInCloudKit:self.keychainZoneID];
1836 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1837 if([record.recordType isEqualToString:SecCKRecordDeviceStateType]) {
1838 record[SecCKRecordKeyState] = CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTLK);
1842 self.keychainZone.flag = true;
1844 [self startCKKSSubsystem];
1845 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1847 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1848 [self performOctagonTLKUpload:self.ckksViews];
1850 // But then, it'll fire off the reset and reach 'ready'
1851 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1852 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1854 // And the zone should have been cleared and re-made
1855 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1858 - (void)testResetCloudKitZoneFromNoTLKIgnoringInactiveDevices {
1859 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1860 OCMExpect([self.suggestTLKUpload trigger]);
1862 self.silentZoneDeletesAllowed = true;
1864 // If CKKS sees a zone it's never going to be able to read, it should reset that zone
1865 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1866 // Save a fake device status here, but modify its creation and modification times to be months ago
1867 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1868 [self putFakeOctagonOnlyDeviceStatusInCloudKit:self.keychainZoneID];
1870 // Put a 'in-circle' TLKShare record, but also modify its creation and modification times
1871 CKKSSOSSelfPeer* untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"untrusted-peer"
1872 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1873 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1874 viewList:self.managedViewList];
1875 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:untrustedPeer to:untrustedPeer zoneID:self.keychainZoneID];
1877 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1878 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1879 record.creationDate = [NSDate distantPast];
1880 record.modificationDate = [NSDate distantPast];
1884 self.keychainZone.flag = true;
1886 [self startCKKSSubsystem];
1887 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1889 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1890 [self performOctagonTLKUpload:self.ckksViews];
1892 // But then, it'll fire off the reset and reach 'ready'
1893 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1894 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1896 // And the zone should have been cleared and re-made
1897 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1900 - (void)testDoNotResetCloudKitZoneDuringBadCircleState {
1901 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1902 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1904 // This test has stuff in CloudKit, but no TLKs.
1905 // CKKS should NOT reset the CK zone.
1906 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1907 self.zones[self.keychainZoneID].flag = true;
1909 [self startCKKSSubsystem];
1911 // But since we're out of circle, this test needs to initialize the zone itself
1912 [self.keychainView beginCloudKitOperation];
1914 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered waitfortrust");
1915 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1917 FakeCKZone* keychainZone = self.zones[self.keychainZoneID];
1918 XCTAssertNotNil(keychainZone, "Should still have a keychain zone");
1919 XCTAssertTrue(keychainZone.flag, "keychain zone should not have been recreated");
1922 - (void)testDoNotResetCloudKitZoneFromWaitForTLKDueToRecentDeviceState {
1923 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1925 // CKKS shouldn't reset this zone, due to a recent device status claiming to have TLKs
1926 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1928 // Also, CKKS _should_ be able to return the key hierarchy if asked before it starts
1929 CKKSResultOperation<CKKSKeySetProviderOperationProtocol>* keysetOp = [self.keychainView findKeySet:NO];
1931 NSDateComponents* offset = [[NSDateComponents alloc] init];
1933 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1934 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1935 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1936 record.creationDate = updateTime;
1937 record.modificationDate = updateTime;
1941 self.keychainZone.flag = true;
1942 [self startCKKSSubsystem];
1944 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1946 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1948 // And, ensure that the keyset op ran and has results
1949 CKKSResultOperation* waitOp = [CKKSResultOperation named:@"test op" withBlock:^{}];
1950 [waitOp addDependency:keysetOp];
1951 [waitOp timeout:2*NSEC_PER_SEC];
1952 [self.operationQueue addOperation:waitOp];
1953 [waitOp waitUntilFinished];
1955 XCTAssert(keysetOp.finished, "Keyset op should have finished");
1956 XCTAssertNil(keysetOp.error, "keyset op should not have errored");
1957 XCTAssertNotNil(keysetOp.keyset, "keyset op should have a keyset");
1958 XCTAssertNotNil(keysetOp.keyset.currentTLKPointer, "keyset should have a current TLK pointer");
1959 XCTAssertEqualObjects(keysetOp.keyset.currentTLKPointer.currentKeyUUID, self.keychainZoneKeys.tlk.uuid, "keyset should match what's in zone");
1962 - (void)testDoNotCloudKitZoneFromWaitForTLKDueToRecentButUntrustedDeviceState {
1963 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1965 // CKKS should reset this zone, even though to a recent device status claiming to have TLKs. The device isn't trusted
1966 self.silentZoneDeletesAllowed = true;
1967 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1968 [self.mockSOSAdapter.trustedPeers removeObject:self.remoteSOSOnlyPeer];
1970 self.keychainZone.flag = true;
1971 [self startCKKSSubsystem];
1973 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1974 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1976 // And ensure it doesn't go on to 'reset'
1977 XCTAssertNotEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:100*NSEC_PER_MSEC], @"Key state should not become 'resetzone'");
1980 - (void)testResetCloudKitZoneFromWaitForTLKDueToLessRecentAndUntrustedDeviceState {
1981 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1982 OCMExpect([self.suggestTLKUpload trigger]);
1984 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1986 // CKKS should reset this zone, even though to a recent device status claiming to have TLKs. The device isn't trusted
1987 self.silentZoneDeletesAllowed = true;
1988 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1989 [self.mockSOSAdapter.trustedPeers removeObject:self.remoteSOSOnlyPeer];
1991 NSDateComponents* offset = [[NSDateComponents alloc] init];
1993 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1994 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1995 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1996 record.creationDate = updateTime;
1997 record.modificationDate = updateTime;
2001 self.keychainZone.flag = true;
2002 [self startCKKSSubsystem];
2003 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
2005 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
2006 [self performOctagonTLKUpload:self.ckksViews];
2008 // Then we should reset.
2009 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2010 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
2012 // And the zone should have been cleared and re-made
2013 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
2016 - (void)testAcceptExistingKeyHierarchy {
2017 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
2018 // Test also begins with the TLK having arrived in the local keychain (via SOS)
2019 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2020 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2021 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2023 // Spin up CKKS subsystem.
2024 [self startCKKSSubsystem];
2026 // The CKKS subsystem should only upload its TLK share
2027 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
2029 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2031 // Verify that there are three local keys, and three local current key records
2032 __weak __typeof(self) weakSelf = self;
2033 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
2034 __strong __typeof(weakSelf) strongSelf = weakSelf;
2035 XCTAssertNotNil(strongSelf, "self exists");
2037 NSError* error = nil;
2039 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
2040 XCTAssertNil(error, "no error fetching keys");
2041 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
2043 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
2044 XCTAssertNil(error, "no error fetching current keys");
2045 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
2049 - (void)testAcceptExistingAndUseKeyHierarchy {
2050 // Test starts with nothing in database, but one in our fake CloudKit.
2051 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2052 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
2053 // But, CKKS shouldn't ever reset the zone
2054 self.keychainZone.flag = true;
2056 [self startCKKSSubsystem];
2057 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:200*NSEC_PER_SEC], "Key state should have become waitfortlk");
2059 // Now, save the TLK to the keychain (to simulate it coming in later via SOS). We'll create a TLK share for ourselves.
2060 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2061 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2063 // Wait for the key hierarchy to sort itself out, to make it easier on this test; see testOnboardOldItemsWithExistingKeyHierarchy for the other test.
2064 // The CKKS subsystem should write its TLK share, but nothing else
2065 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
2067 // We expect a single record to be uploaded for each key class
2068 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2069 [self addGenericPassword: @"data" account: @"account-delete-me"];
2070 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2072 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2073 [self addGenericPassword:@"asdf"
2074 account:@"account-class-A"
2076 access:(id)kSecAttrAccessibleWhenUnlocked
2077 expecting:errSecSuccess
2078 message:@"Adding class A item"];
2079 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2080 XCTAssertTrue(self.keychainZone.flag, "Keychain zone shouldn't have been reset");
2083 - (void)testAcceptExistingKeyHierarchyDespiteLocked {
2084 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
2085 // Test also begins with the TLK having arrived in the local keychain (via SOS)
2087 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2088 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2090 self.aksLockState = true;
2091 [self.lockStateTracker recheck];
2093 // Spin up CKKS subsystem.
2094 [self startCKKSSubsystem];
2096 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:20*NSEC_PER_SEC], "Key state should have become waitforunlock");
2098 // CKKS will give itself a TLK Share
2099 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2101 // Now that all operations are complete, 'unlock' AKS
2102 self.aksLockState = false;
2103 [self.lockStateTracker recheck];
2105 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2106 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
2108 // Verify that there are three local keys, and three local current key records
2109 __weak __typeof(self) weakSelf = self;
2110 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
2111 __strong __typeof(weakSelf) strongSelf = weakSelf;
2112 XCTAssertNotNil(strongSelf, "self exists");
2114 NSError* error = nil;
2116 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
2117 XCTAssertNil(error, "no error fetching keys");
2118 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
2120 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
2121 XCTAssertNil(error, "no error fetching current keys");
2122 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
2126 - (void)testReceiveClassCWhileALocked {
2127 // Test starts with a key hierarchy already existing.
2128 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
2129 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2130 [self startCKKSSubsystem];
2132 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
2134 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
2135 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2137 [self findGenericPassword:@"classCItem" expecting:errSecItemNotFound];
2138 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
2140 // 'Lock' the keybag
2141 self.aksLockState = true;
2142 [self.lockStateTracker recheck];
2144 XCTAssertNotNil(self.keychainZoneKeys, "Have zone keys for zone");
2145 XCTAssertNotNil(self.keychainZoneKeys.classA, "Have class A key for zone");
2146 XCTAssertNotNil(self.keychainZoneKeys.classC, "Have class C key for zone");
2148 [self.keychainView.stateMachine handleFlag:CKKSFlagKeyStateProcessRequested];
2150 // And ensure we end up back in 'readypendingunlock': we have the keys, we're just locked now
2151 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
2152 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
2154 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"classCItem" key:self.keychainZoneKeys.classC]];
2155 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-FFFF-FFFF-FFFF-5A507ACB2D85" withAccount:@"classAItem" key:self.keychainZoneKeys.classA]];
2157 CKKSResultOperation* op = self.keychainView.resultsOfNextProcessIncomingQueueOperation;
2158 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
2159 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2160 // The processing op should NOT error, even though it didn't manage to process the classA item
2161 XCTAssertNil(op.error, "no error while failing to process a class A item");
2163 CKKSResultOperation* erroringOp = [self.keychainView processIncomingQueue:true];
2164 [erroringOp waitUntilFinished];
2165 XCTAssertNotNil(erroringOp.error, "error exists while processing a class A item");
2167 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
2168 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
2170 self.aksLockState = false;
2171 [self.lockStateTracker recheck];
2172 [self.keychainView waitUntilAllOperationsAreFinished];
2174 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
2175 [self findGenericPassword:@"classAItem" expecting:errSecSuccess];
2178 - (void)testRestartWhileLocked {
2179 [self startCKKSSubsystem];
2180 [self performOctagonTLKUpload:self.ckksViews];
2182 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
2184 // 'Lock' the keybag
2185 self.aksLockState = true;
2186 [self.lockStateTracker recheck];
2188 [self.keychainView halt];
2189 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
2190 [self.keychainView beginCloudKitOperation];
2191 [self beginSOSTrustedViewOperation:self.keychainView];
2193 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
2195 self.aksLockState = false;
2196 [self.lockStateTracker recheck];
2198 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
2201 - (void)testExternalKeyRoll {
2202 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
2203 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2204 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2205 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2207 // Spin up CKKS subsystem.
2208 [self startCKKSSubsystem];
2210 // The CKKS subsystem should not try to write anything to the CloudKit database.
2211 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2213 __weak __typeof(self) weakSelf = self;
2215 // We expect a single record to be uploaded.
2216 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2218 [self addGenericPassword: @"data" account: @"account-delete-me"];
2220 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2221 [self waitForCKModifications];
2223 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2224 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2225 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2227 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
2229 // Make life easy on this test; testAcceptKeyConflictAndUploadReencryptedItem will check the case when we don't receive the notification
2230 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2232 // Just in extra case of threading issues, force a reexamination of the key hierarchy
2233 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
2234 [self.keychainView _onqueuePokeKeyStateMachine];
2235 return CKKSDatabaseTransactionCommit;
2238 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
2240 // Verify that there are six local keys, and three local current key records
2241 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
2242 __strong __typeof(weakSelf) strongSelf = weakSelf;
2243 XCTAssertNotNil(strongSelf, "self exists");
2245 NSError* error = nil;
2246 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:self.keychainZoneID error:&error];
2247 XCTAssertNil(error, "no error fetching keys");
2248 XCTAssertEqual(keys.count, 6u, "Six keys in local database");
2250 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
2251 XCTAssertNil(error, "no error fetching current keys");
2252 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
2254 for(CKKSCurrentKeyPointer* key in currentkeys) {
2255 if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
2256 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.tlk.uuid);
2257 } else if([key.keyclass isEqualToString: SecCKKSKeyClassA]) {
2258 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classA.uuid);
2259 } else if([key.keyclass isEqualToString: SecCKKSKeyClassC]) {
2260 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classC.uuid);
2262 XCTFail("Unknown key class: %@", key.keyclass);
2267 // We expect a single record to be uploaded.
2268 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2270 // TODO: remove this by writing code for item reencrypt after key arrival
2271 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
2272 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2274 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
2276 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2279 - (void)testAcceptKeyConflictAndUploadReencryptedItem {
2280 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
2281 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2282 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2283 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2285 [self startCKKSSubsystem];
2286 [self.keychainView waitUntilAllOperationsAreFinished];
2288 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
2290 // We expect a single record to be uploaded.
2291 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2293 [self addGenericPassword: @"data" account: @"account-delete-me"];
2295 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2296 [self waitForCKModifications];
2297 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
2299 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2301 // Do not trigger a notification here. This should cause a conflict updating the current key records
2303 // We expect a single record to be uploaded, but that the write will be rejected
2304 // We then expect that item to be reuploaded with the current key
2306 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
2307 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
2308 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2310 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under rolled class C key in hierarchy"]];
2312 // New key arrives via SOS!
2313 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2314 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2316 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2319 - (void)testAcceptKeyConflictAndUploadReencryptedItems {
2320 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
2321 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2322 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2323 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2325 [self startCKKSSubsystem];
2326 [self.keychainView waitUntilAllOperationsAreFinished];
2328 // We expect a single record to be uploaded.
2329 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
2330 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2332 [self addGenericPassword: @"data" account: @"account-delete-me"];
2334 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2335 [self waitForCKModifications];
2337 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2339 // Do not trigger a notification here. This should cause a conflict updating the current key records
2341 // We expect a single record to be uploaded, but that the write will be rejected
2342 // We then expect that item to be reuploaded with the current key
2344 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
2345 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
2346 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key-2"];
2347 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2349 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
2350 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under rolled class C key in hierarchy"]];
2352 // New key arrives via SOS!
2353 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2354 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2356 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2359 - (void)testAcceptKeyHierarchyResetAndUploadReencryptedItem {
2360 // Test starts with nothing in CloudKit. CKKS uploads a key hierarchy, then it's silently replaced.
2361 // CKKS should notice the replacement, and reupload the item.
2363 [self startCKKSSubsystem];
2365 [self performOctagonTLKUpload:self.ckksViews];
2366 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2368 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
2370 // We expect a single record to be uploaded.
2371 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2373 [self addGenericPassword: @"data" account: @"account-delete-me"];
2375 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2376 [self waitForCKModifications];
2378 // A new peer arrives and resets the world! It sends us a share, though.
2379 CKKSSOSSelfPeer* remotePeer1 = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"remote-peer1"
2380 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
2381 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
2382 viewList:self.managedViewList];
2383 [self.mockSOSAdapter.trustedPeers addObject:remotePeer1];
2385 NSString* classCUUID = self.keychainZoneKeys.classC.uuid;
2387 self.zones[self.keychainZoneID] = [[FakeCKZone alloc] initZone:self.keychainZoneID];
2388 self.keys[self.keychainZoneID] = nil;
2389 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2390 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:remotePeer1 zoneID:self.keychainZoneID];
2392 XCTAssertNotEqual(classCUUID, self.keychainZoneKeys.classC.uuid, @"Class C UUID should have changed");
2394 // Upon adding an item, we expect a failed OQO, then another OQO with the two items (encrypted correctly)
2395 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
2397 [self expectCKModifyItemRecords:2
2398 currentKeyPointerRecords:1
2399 zoneID:self.keychainZoneID
2400 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2402 // We also expect a self share upload, once CKKS figures out the right key hierarchy
2403 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2405 [self addGenericPassword: @"data" account: @"account-delete-me-after-reset"];
2407 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2410 - (void)testRecoverFromRequestKeyRefetchWithoutRolling {
2411 // Simply requesting a key state refetch shouldn't roll the key hierarchy.
2413 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2415 // Spin up CKKS subsystem.
2416 [self startCKKSSubsystem];
2418 // Items should upload.
2419 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2420 [self addGenericPassword: @"data" account: @"account-delete-me"];
2421 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2423 [self waitForCKModifications];
2425 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2427 // CKKS should not roll the keys while progressing back to 'ready', but it will fetch once
2428 self.silentFetchesAllowed = false;
2429 [self expectCKFetch];
2431 [self.keychainView.stateMachine handleFlag:CKKSFlagFetchRequested];
2433 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
2434 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2437 - (void)testRecoverFromIncrementedCurrentKeyPointerEtag {
2438 // CloudKit sometimes reports the current key pointers have changed (etag mismatch), but their content hasn't.
2439 // In this case, CKKS shouldn't roll the TLK.
2441 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2443 // Spin up CKKS subsystem.
2444 [self startCKKSSubsystem];
2445 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
2446 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // just to be sure it's fetched
2448 // Items should upload.
2449 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2450 [self addGenericPassword: @"data" account: @"account-delete-me"];
2451 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2453 [self waitForCKModifications];
2455 // Bump the etag on the class C current key record, but don't change any data
2456 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
2457 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
2458 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
2460 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
2461 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
2463 // Add another item. This write should fail, then CKKS should recover without rolling the key hierarchy or issuing a fetch.
2464 self.silentFetchesAllowed = false;
2465 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
2466 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2467 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
2468 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2471 - (void)testRecoverMultipleItemsFromIncrementedCurrentKeyPointerEtag {
2472 // CloudKit sometimes reports the current key pointers have changed (etag mismatch), but their content hasn't.
2473 // In this case, CKKS shouldn't roll the TLK.
2474 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2476 // Spin up CKKS subsystem.
2477 [self startCKKSSubsystem];
2478 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
2479 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // just to be sure it's fetched
2481 // Items should upload.
2482 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2483 [self addGenericPassword: @"data" account: @"account-delete-me"];
2484 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2486 [self waitForCKModifications];
2488 // Bump the etag on the class C current key record, but don't change any data
2489 CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName: @"classC" zoneID: self.keychainZoneID];
2490 CKRecord* currentClassC = self.keychainZone.currentDatabase[currentClassCID];
2491 XCTAssertNotNil(currentClassC, "Should have the class C current key pointer record");
2493 [self.keychainZone addCKRecordToZone:[currentClassC copy]];
2494 XCTAssertNotEqualObjects(currentClassC.etag, self.keychainZone.currentDatabase[currentClassCID].etag, "Etag should have changed");
2496 // Add another item. This write should fail, then CKKS should recover without rolling the key hierarchy or issuing a fetch.
2497 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
2498 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
2499 ckksnotice_global("ckks", "releasing outgoing-queue hold");
2502 self.silentFetchesAllowed = false;
2503 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
2504 [self expectCKModifyItemRecords:2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2505 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
2506 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
2508 [self.operationQueue addOperation: self.keychainView.holdOutgoingQueueOperation];
2509 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2512 - (void)testOnboardOldItemsCreatingKeyHierarchy {
2513 // 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
2515 // Test starts with nothing in CloudKit, and CKKS blocked. Add one item without a UUID...
2517 SecCKKSTestSetDisableAutomaticUUID(true);
2518 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
2520 // and an item with a UUID...
2521 SecCKKSTestSetDisableAutomaticUUID(false);
2522 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
2524 // We then expect an upload of the added items
2525 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2527 [self startCKKSSubsystem];
2528 [self performOctagonTLKUpload:self.ckksViews];
2530 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2533 - (void)testOnboardOldItemsWithExistingKeyHierarchy {
2534 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2536 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2537 [self addGenericPassword: @"data" account: @"account-delete-me"];
2539 [self startCKKSSubsystem];
2540 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2543 - (void)testOnboardOldItemsWithExistingKeyHierarchyExtantTLK {
2544 // Test starts key hierarchy in our fake CloudKit, the TLK arrived in the local keychain, and CKKS blocked.
2545 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2546 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2547 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2549 // Add one item without a UUID...
2550 SecCKKSTestSetDisableAutomaticUUID(true);
2551 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
2553 // and an item with a UUID...
2554 SecCKKSTestSetDisableAutomaticUUID(false);
2555 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
2557 // 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
2558 // We expect a single record to be uploaded.
2559 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2561 // Spin up CKKS subsystem.
2562 [self startCKKSSubsystem];
2564 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2567 - (void)testOnboardOldItemsWithExistingKeyHierarchyLateTLK {
2568 // Test starts key hierarchy in our fake CloudKit, and CKKS blocked.
2569 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2570 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
2571 self.keychainZone.flag = true;
2573 // Add one item without a UUID...
2574 SecCKKSTestSetDisableAutomaticUUID(true);
2575 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
2577 // and an item with a UUID...
2578 SecCKKSTestSetDisableAutomaticUUID(false);
2579 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
2581 // 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
2583 // Spin up CKKS subsystem.
2584 [self startCKKSSubsystem];
2585 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should have become waitfortlk");
2587 // Now, save the TLK to the keychain (to simulate it coming in via SOS).
2588 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2589 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2591 // We expect a single record to be uploaded.
2592 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2594 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2595 XCTAssertTrue(self.keychainZone.flag, "Keychain zone shouldn't have been reset");
2598 - (void)testOnboardOldItemMatchingExistingCKKSItem {
2599 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
2601 NSString* itemAccount = @"account-delete-me";
2602 [self addGenericPassword:@"password" account:itemAccount];
2604 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
2605 [self.keychainZone addToZone:ckr];
2607 [self startCKKSSubsystem];
2609 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
2611 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
2612 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2614 [self findGenericPassword:itemAccount expecting:errSecSuccess];
2616 // And, the local item should now match the UUID downloaded from CKKS
2617 [self checkGenericPasswordStoredUUID:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" account:itemAccount];
2620 - (void)testResync {
2621 // We need to set up a desynced situation to test our resync.
2622 // First, let CKKS start up and send several items to CloudKit (that we'll then desync!)
2623 __block NSError* error = nil;
2625 // Test starts with keys in CloudKit (so we can create items later)
2626 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2627 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
2628 [self saveTLKMaterialToKeychain:self.keychainZoneID];
2630 [self addGenericPassword: @"data" account: @"first"];
2631 [self addGenericPassword: @"data" account: @"second"];
2632 [self addGenericPassword: @"data" account: @"third"];
2633 [self addGenericPassword: @"data" account: @"fourth"];
2634 [self addGenericPassword: @"data" account: @"fifth"];
2635 [self addGenericPassword: @"data" account: @"sixth"];
2636 NSUInteger passwordCount = 6u;
2638 [self checkGenericPassword: @"data" account: @"first"];
2639 [self checkGenericPassword: @"data" account: @"second"];
2640 [self checkGenericPassword: @"data" account: @"third"];
2641 [self checkGenericPassword: @"data" account: @"fourth"];
2642 [self checkGenericPassword: @"data" account: @"fifth"];
2643 [self checkGenericPassword: @"data" account: @"sixth"];
2645 [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2647 [self startCKKSSubsystem];
2649 // Wait for uploads to happen
2650 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2651 [self waitForCKModifications];
2652 // One TLK share record
2653 XCTAssertEqual(self.keychainZone.currentDatabase.count, SYSTEM_DB_RECORD_COUNT+passwordCount+1, "Have SYSTEM_DB_RECORD_COUNT+passwordCount+1 objects in cloudkit");
2655 // Now, corrupt away!
2656 // Extract all passwordCount items for Corruption
2657 NSArray<CKRecord*>* items = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
2658 XCTAssertEqual(items.count, passwordCount, "Have %lu Items in cloudkit", (unsigned long)passwordCount);
2660 // For the first record, delete all traces of it from CKKS. But! it remains in local keychain.
2661 // Expected outcome: CKKS resyncs; item exists again.
2662 CKRecord* delete = items[0];
2663 NSString* deleteAccount = [[self decryptRecord: delete] objectForKey: (__bridge id) kSecAttrAccount];
2664 XCTAssertNotNil(deleteAccount, "received an account for the local delete object");
2666 __weak __typeof(self) weakSelf = self;
2667 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
2668 __strong __typeof(weakSelf) strongSelf = weakSelf;
2669 XCTAssertNotNil(strongSelf, "self exists");
2671 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2673 [ckme deleteFromDatabase: &error];
2675 XCTAssertNil(error, "no error removing CKME");
2676 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2678 [oqe deleteFromDatabase: &error];
2680 XCTAssertNil(error, "no error removing OQE");
2681 CKKSIncomingQueueEntry* iqe = [CKKSIncomingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2683 [iqe deleteFromDatabase: &error];
2685 XCTAssertNil(error, "no error removing IQE");
2686 return CKKSDatabaseTransactionCommit;
2689 // For the second record, delete all traces of it from CloudKit.
2690 // Expected outcome: deleted locally
2691 CKRecord* remoteDelete = items[1];
2692 NSString* remoteDeleteAccount = [[self decryptRecord: remoteDelete] objectForKey: (__bridge id) kSecAttrAccount];
2693 XCTAssertNotNil(remoteDeleteAccount, "received an account for the remote delete object");
2695 [self.keychainZone deleteCKRecordIDFromZone: remoteDelete.recordID];
2696 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
2697 [database removeObjectForKey: remoteDelete.recordID];
2700 // The third record gets modified in CloudKit, but not locally.
2701 // Expected outcome: use the CloudKit version
2702 CKRecord* remoteDataChanged = items[2];
2703 NSMutableDictionary* remoteDataDictionary = [[self decryptRecord: remoteDataChanged] mutableCopy];
2704 NSString* remoteDataChangedAccount = [remoteDataDictionary objectForKey: (__bridge id) kSecAttrAccount];
2705 XCTAssertNotNil(remoteDataChangedAccount, "Received an account for the remote-data-changed object");
2706 remoteDataDictionary[(__bridge id) kSecValueData] = [@"CloudKitWins" dataUsingEncoding: NSUTF8StringEncoding];
2708 CKRecord* newData = [self newRecord: remoteDataChanged.recordID withNewItemData: remoteDataDictionary];
2709 [self.keychainZone addToZone: newData];
2710 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
2711 database[remoteDataChanged.recordID] = newData;
2714 // The fourth record stays in-sync. Good work, everyone!
2715 // Expected outcome: stays in-sync
2716 NSString* insyncAccount = [[self decryptRecord: items[3]] objectForKey: (__bridge id) kSecAttrAccount];
2717 XCTAssertNotNil(insyncAccount, "Received an account for the in-sync object");
2719 // The fifth record is updated locally, but CKKS didn't get the notification, and so the local CKMirror and CloudKit don't have it
2720 // Expected outcome: local change should be steamrolled by the cloud version
2721 CKRecord* localDataChanged = items[4];
2722 NSMutableDictionary* localDataDictionary = [[self decryptRecord: localDataChanged] mutableCopy];
2723 NSString* localDataChangedAccount = [localDataDictionary objectForKey: (__bridge id) kSecAttrAccount];
2725 [self updateGenericPassword:@"newpassword" account:localDataChangedAccount];
2726 [self checkGenericPassword:@"newpassword" account:localDataChangedAccount];
2729 // The sixth record matches what's in CloudKit, but the local UUID has changed (and CKKS didn't notice, for whatever reason)
2730 CKRecord* uuidMismatch = items[5];
2731 NSMutableDictionary* uuidMisMatchDictionary = [[self decryptRecord:uuidMismatch] mutableCopy];
2732 NSString* uuidMismatchAccount = uuidMisMatchDictionary[(__bridge id)kSecAttrAccount];
2733 NSString* newUUID = @"55463F83-3AAE-462D-B95F-2FA6AD088980";
2735 [self setGenericPasswordStoredUUID:newUUID account:uuidMismatchAccount];
2736 [self checkGenericPasswordStoredUUID:newUUID account:uuidMismatchAccount];
2739 // To make this more challenging, CK returns the refetch in multiple batches. This shouldn't affect the resync...
2740 CKServerChangeToken* ck1 = self.keychainZone.currentChangeToken;
2741 self.silentFetchesAllowed = false;
2742 [self expectCKFetch];
2743 [self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
2744 // Assert that the fetch is happening with the change token we paused at before
2745 CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.keychainZoneID].previousServerChangeToken;
2746 if(changeToken && [changeToken isEqual:ck1]) {
2751 } runBeforeFinished:^{}];
2753 self.keychainZone.limitFetchTo = ck1;
2754 self.keychainZone.limitFetchError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkFailure userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:4]}];
2756 // The seventh record gets magically added to CloudKit, but CKKS has never heard of it
2757 // (emulates a lost record on the client, but that CloudKit already believes it's sent the record for)
2758 // Expected outcome: added to local keychain
2759 NSString* remoteOnlyAccount = @"remote-only";
2760 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount: remoteOnlyAccount];
2761 [self.keychainZone addToZone: ckr];
2762 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
2763 database[ckr.recordID] = ckr;
2766 ckksnotice("ckksresync", self.keychainView, "local delete: %@ %@", delete.recordID.recordName, deleteAccount);
2767 ckksnotice("ckksresync", self.keychainView, "Remote deletion: %@ %@", remoteDelete.recordID.recordName, remoteDeleteAccount);
2768 ckksnotice("ckksresync", self.keychainView, "Remote data changed: %@ %@", remoteDataChanged.recordID.recordName, remoteDataChangedAccount);
2769 ckksnotice("ckksresync", self.keychainView, "in-sync: %@ %@", items[3].recordID.recordName, insyncAccount);
2770 ckksnotice("ckksresync", self.keychainView, "local update: %@ %@", items[4].recordID.recordName, localDataChangedAccount);
2771 ckksnotice("ckksresync", self.keychainView, "uuid mismatch: %@ %@", items[5].recordID.recordName, uuidMismatchAccount);
2772 ckksnotice("ckksresync", self.keychainView, "Remote only: %@ %@", ckr.recordID.recordName, remoteOnlyAccount);
2774 CKKSSynchronizeOperation* resyncOperation = [self.keychainView resyncWithCloud];
2775 [resyncOperation waitUntilFinished];
2777 XCTAssertNil(resyncOperation.error, "No error during the resync operation");
2779 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2781 // Now do some checking. Remember, we don't know which record we corrupted, so use the parsed account variables to check.
2783 [self findGenericPassword: deleteAccount expecting: errSecSuccess];
2784 [self findGenericPassword: remoteDeleteAccount expecting: errSecItemNotFound];
2785 [self findGenericPassword: remoteDataChangedAccount expecting: errSecSuccess];
2786 [self findGenericPassword: insyncAccount expecting: errSecSuccess];
2787 [self findGenericPassword: localDataChangedAccount expecting: errSecSuccess];
2788 [self findGenericPassword: uuidMismatchAccount expecting: errSecSuccess];
2789 [self findGenericPassword: remoteOnlyAccount expecting: errSecSuccess];
2791 [self checkGenericPassword: @"data" account: deleteAccount];
2792 //[self checkGenericPassword: @"data" account: remoteDeleteAccount];
2793 [self checkGenericPassword: @"CloudKitWins" account: remoteDataChangedAccount];
2794 [self checkGenericPassword: @"data" account: insyncAccount];
2795 [self checkGenericPassword:@"data" account:localDataChangedAccount];
2796 [self checkGenericPassword:@"data" account:uuidMismatchAccount];
2797 [self checkGenericPassword: @"data" account: remoteOnlyAccount];
2799 [self checkGenericPasswordStoredUUID:uuidMismatch.recordID.recordName account:uuidMismatchAccount];
2801 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
2802 __strong __typeof(weakSelf) strongSelf = weakSelf;
2803 XCTAssertNotNil(strongSelf, "self exists");
2805 CKKSMirrorEntry* ckme = nil;
2807 ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2808 XCTAssertNil(error);
2809 XCTAssertNotNil(ckme);
2811 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDelete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2812 XCTAssertNil(error);
2813 XCTAssertNil(ckme); // deleted!
2815 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDataChanged.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2816 XCTAssertNil(error);
2817 XCTAssertNotNil(ckme);
2819 ckme = [CKKSMirrorEntry tryFromDatabase:items[3].recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2820 XCTAssertNil(error);
2821 XCTAssertNotNil(ckme);
2823 ckme = [CKKSMirrorEntry tryFromDatabase:items[4].recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2824 XCTAssertNil(error);
2825 XCTAssertNotNil(ckme);
2827 ckme = [CKKSMirrorEntry tryFromDatabase:ckr.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
2828 XCTAssertNil(error);
2829 XCTAssertNotNil(ckme);
2833 - (void)testResyncItemsMissingFromLocalKeychain {
2834 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2837 // one password correctly synced between local keychain and CloudKit
2838 // one password incorrectly disappeared from local keychain, but in mirror table
2839 // one password sitting in the outgoing queue
2840 // one password sitting in the incoming queue
2842 // Add and sync two passwords
2843 [self addGenericPassword: @"data" account: @"first"];
2844 [self addGenericPassword: @"data" account: @"second"];
2846 [self checkGenericPassword: @"data" account: @"first"];
2847 [self checkGenericPassword: @"data" account: @"second"];
2849 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2850 [self startCKKSSubsystem];
2851 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2852 [self waitForCKModifications];
2854 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
2855 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2857 // Now, place an item in the outgoing queue
2859 //[self addGenericPassword: @"data" account: @"third"];
2860 //[self checkGenericPassword: @"data" account: @"third"];
2862 // Now, corrupt away!
2863 // Extract all passwordCount items for Corruption
2864 NSArray<CKRecord*>* items = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
2865 XCTAssertEqual(items.count, 2u, "Have %lu Items in cloudkit", (unsigned long)2u);
2867 // For the first record, surreptitiously remove from local keychain
2868 CKRecord* remove = items[0];
2869 NSString* removeAccount = [[self decryptRecord:remove] objectForKey:(__bridge id)kSecAttrAccount];
2870 XCTAssertNotNil(removeAccount, "received an account for the local delete object");
2872 NSURL* kcpath = (__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)@"keychain-2-debug.db");
2874 sqlite3_open([[kcpath path] UTF8String], &db);
2875 NSString* query = [NSString stringWithFormat:@"DELETE FROM genp WHERE uuid=\"%@\"", remove.recordID.recordName];
2876 char* sqlerror = NULL;
2877 XCTAssertEqual(SQLITE_OK, sqlite3_exec(db, [query UTF8String], NULL, NULL, &sqlerror), "SQL deletion shouldn't error");
2878 XCTAssertTrue(sqlerror == NULL, "No error string should have been returned: %s", sqlerror);
2880 sqlite3_free(sqlerror);
2885 // The second record is kept in-sync
2887 // Now, add an in-flight change (for record 3)
2888 [self holdCloudKitModifications];
2889 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2890 [self addGenericPassword:@"data" account:@"third"];
2891 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2893 // For the fourth, add a new record but prevent incoming queue processing
2894 self.keychainView.holdIncomingQueueOperation = [CKKSResultOperation named:@"hold-incoming" withBlock:^{}];
2896 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"fourth"];
2897 [self.keychainZone addToZone:ckr];
2898 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
2900 // Now, where are we....
2901 CKKSScanLocalItemsOperation* scanLocal = [self.keychainView scanLocalItems:@"test-scan"];
2902 // This operation will wait for the CKKSOutgoingQueue operation (currently held writing to cloudkit) to finish before beginning
2904 // Allow everything to proceed
2905 [self releaseCloudKitModificationHold];
2907 [scanLocal waitUntilFinished];
2908 XCTAssertEqual(scanLocal.missingLocalItemsFound, 1u, "Should have found one missing item");
2910 [self.operationQueue addOperation:self.keychainView.holdIncomingQueueOperation];
2912 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2914 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2915 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
2917 // And ensure that all four items are present again
2918 [self findGenericPassword: @"first" expecting: errSecSuccess];
2919 [self findGenericPassword: @"second" expecting: errSecSuccess];
2920 [self findGenericPassword: @"third" expecting: errSecSuccess];
2921 [self findGenericPassword: @"fourth" expecting: errSecSuccess];
2924 - (void)testScanItemsChangedInLocalKeychain {
2925 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2927 // Add and sync two passwords
2928 NSString* itemAccount = @"first";
2929 [self addGenericPassword:@"data" account:itemAccount];
2930 [self addGenericPassword:@"data" account:@"second"];
2932 [self checkGenericPassword:@"data" account:itemAccount];
2933 [self checkGenericPassword:@"data" account:@"second"];
2935 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2936 [self startCKKSSubsystem];
2937 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2938 [self waitForCKModifications];
2940 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
2941 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2943 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
2944 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
2946 // Now, have CKKS miss an update
2948 [self updateGenericPassword:@"newpassword" account:itemAccount];
2949 [self checkGenericPassword:@"newpassword" account:itemAccount];
2952 // Now, where are we....
2953 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
2954 checkItem:[self checkPasswordBlock:self.keychainZoneID account:itemAccount password:@"newpassword"]];
2956 CKKSScanLocalItemsOperation* scanLocal = [self.keychainView scanLocalItems:@"test-scan"];
2957 [scanLocal waitUntilFinished];
2959 XCTAssertEqual(scanLocal.recordsAdded, 1u, "Should have added a single record");
2961 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2963 // And ensure that all four items are present again
2964 [self findGenericPassword: @"first" expecting: errSecSuccess];
2965 [self findGenericPassword: @"second" expecting: errSecSuccess];
2968 - (void)testEnsureScanOccursOnNextStartIfCancelled {
2969 // We want to set up a situation where a CKKSScanLocalItemsOperation is cancelled by daemon quitting.
2970 NSString* itemAccount = @"first";
2971 [self addGenericPassword:@"data" account:itemAccount];
2972 [self addGenericPassword:@"data" account:@"second"];
2974 // We're going to pretend that the scan doesn't happen due to daemon restart
2975 SecCKKSSetTestSkipScan(true);
2977 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID]; // Make life easy for this test.
2979 [self startCKKSSubsystem];
2980 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2981 [self waitForCKModifications];
2983 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
2985 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
2986 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
2988 // CKKS should perform normally if new items are added
2989 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2990 [self addGenericPassword:@"found" account:@"after-setup"];
2991 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2993 // Now, simulate a restart
2994 SecCKKSSetTestSkipScan(false);
2996 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
2998 [self.keychainView halt];
2999 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
3000 [self.keychainView beginCloudKitOperation];
3001 [self beginSOSTrustedViewOperation:self.keychainView];
3003 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3005 [self findGenericPassword:@"first" expecting:errSecSuccess];
3006 [self findGenericPassword:@"second" expecting:errSecSuccess];
3009 - (void)testResyncLocal {
3010 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3011 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3012 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3014 [self addGenericPassword: @"data" account: @"first"];
3015 [self addGenericPassword: @"data" account: @"second"];
3016 NSUInteger passwordCount = 2u;
3018 [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
3019 [self startCKKSSubsystem];
3021 // Wait for uploads to happen
3022 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3023 [self waitForCKModifications];
3025 // Local resyncs shouldn't fetch clouds.
3026 self.silentFetchesAllowed = false;
3028 [self deleteGenericPassword:@"first"];
3029 [self deleteGenericPassword:@"second"];
3032 // And they're gone!
3033 [self findGenericPassword:@"first" expecting:errSecItemNotFound];
3034 [self findGenericPassword:@"second" expecting:errSecItemNotFound];
3036 CKKSLocalSynchronizeOperation* op = [self.keychainView resyncLocal];
3037 [op waitUntilFinished];
3038 XCTAssertNil(op.error, "Shouldn't be an error resyncing locally");
3040 // And they're back!
3041 [self checkGenericPassword: @"data" account: @"first"];
3042 [self checkGenericPassword: @"data" account: @"second"];
3045 - (void)testPlistRestoreResyncsLocal {
3046 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3047 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3048 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3050 [self addGenericPassword: @"data" account: @"first"];
3051 [self addGenericPassword: @"data" account: @"second"];
3052 NSUInteger passwordCount = 2u;
3054 [self checkGenericPassword: @"data" account: @"first"];
3055 [self checkGenericPassword: @"data" account: @"second"];
3057 [self expectCKModifyItemRecords:passwordCount currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
3058 [self startCKKSSubsystem];
3060 // Wait for uploads to happen
3061 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3062 [self waitForCKModifications];
3065 // This 'restores' a plist keychain backup
3066 // That will kick off a local resync in CKKS, so hold that until we're ready...
3067 self.keychainView.holdLocalSynchronizeOperation = [CKKSResultOperation named:@"hold-local-synchronize" withBlock:^{}];
3069 // Local resyncs shouldn't fetch clouds.
3070 self.silentFetchesAllowed = false;
3072 CFErrorRef cferror = NULL;
3073 kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
3074 CFErrorRef cfcferror = NULL;
3076 bool ret = SecServerImportKeychainInPlist(dbt, SecSecurityClientGet(), KEYBAG_NONE, KEYBAG_NONE,
3077 (__bridge CFDictionaryRef)@{}, kSecBackupableItemFilter, false, &cfcferror);
3079 XCTAssertNil(CFBridgingRelease(cfcferror), "Shouldn't error importing a 'backup'");
3080 XCTAssert(ret, "Importing a 'backup' should have succeeded");
3083 XCTAssertNil(CFBridgingRelease(cferror), "Shouldn't error mucking about in the db");
3085 // Restore is additive so original items stick around
3086 [self findGenericPassword:@"first" expecting:errSecSuccess];
3087 [self findGenericPassword:@"second" expecting:errSecSuccess];
3089 // Allow the local resync to continue...
3090 [self.operationQueue addOperation:self.keychainView.holdLocalSynchronizeOperation];
3091 [self.keychainView waitForOperationsOfClass:[CKKSLocalSynchronizeOperation class]];
3093 // Items are still here!
3094 [self checkGenericPassword: @"data" account: @"first"];
3095 [self checkGenericPassword: @"data" account: @"second"];
3098 - (void)testRestartWithoutRefetch {
3099 // Restarting the CKKS operation should check that it's been 15 minutes since the last fetch before it fetches again. Simulate this.
3100 [self startCKKSSubsystem];
3101 [self performOctagonTLKUpload:self.ckksViews];
3103 [self waitForCKModifications];
3104 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3106 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3108 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
3109 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3111 // Tear down the CKKS object and disallow fetches
3112 [self.keychainView halt];
3113 self.silentFetchesAllowed = false;
3115 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
3116 [self beginSOSTrustedViewOperation:self.keychainView];
3117 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3118 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3120 XCTAssertFalse(self.keychainView.initiatedLocalScan, "Should not have initiated a local items scan due to a restart with a recent fetch");
3122 // Okay, cool, rad, now let's set the date to be very long ago and check that there's positively a fetch
3123 [self.keychainView halt];
3124 self.silentFetchesAllowed = false;
3126 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
3127 NSError* error = nil;
3128 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
3130 XCTAssertNil(error, "no error pulling ckse from database");
3131 XCTAssertNotNil(ckse, "received a ckse");
3133 ckse.lastFetchTime = [NSDate distantPast];
3134 [ckse saveToDatabase: &error];
3135 XCTAssertNil(error, "no error saving to database");
3136 return CKKSDatabaseTransactionCommit;
3139 [self expectCKFetch];
3140 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
3141 [self beginSOSTrustedViewOperation:self.keychainView];
3142 [self.keychainView waitForKeyHierarchyReadiness];
3143 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3145 XCTAssertFalse(self.keychainView.initiatedLocalScan, "Should not have initiated a local items scan due to a restart (when we haven't fetched in a while, but did scan recently)");
3147 // Now restart again, but cause a scan to occur
3148 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
3149 NSError* error = nil;
3150 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
3151 XCTAssertNil(error, "no error pulling ckse from database");
3152 XCTAssertNotNil(ckse, "received a ckse");
3154 ckse.lastLocalKeychainScanTime = [NSDate distantPast];
3155 [ckse saveToDatabase:&error];
3156 XCTAssertNil(error, "no error saving to database");
3157 return CKKSDatabaseTransactionCommit;
3160 [self.keychainView halt];
3161 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
3162 [self beginSOSTrustedViewOperation:self.keychainView];
3163 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3164 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3166 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
3167 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3169 XCTAssertTrue(self.keychainView.initiatedLocalScan, "Should have initiated a local items scan due to 24-hr notification");
3172 - (void)testFetchAndScanOn24HrNotification {
3173 // Every 24 hrs, CKKS should fetch if there hasn't been a fetch in a while.
3174 [self startCKKSSubsystem];
3175 [self performOctagonTLKUpload:self.ckksViews];
3177 [self waitForCKModifications];
3178 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3180 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3182 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
3183 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3185 // We now get the 24-hr notification. Set this bit for later checking
3186 XCTAssertTrue(self.keychainView.initiatedLocalScan, "Should have initiated a local items scan during bringup");
3187 self.keychainView.initiatedLocalScan = NO;
3189 self.silentFetchesAllowed = false;
3191 [self.keychainView xpc24HrNotification];
3193 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3194 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3196 XCTAssertFalse(self.keychainView.initiatedLocalScan, "Should not have initiated a local items scan due to a 24-hr notification with a recent fetch");
3198 // Okay, cool, rad, now let's set the last local-keychain-scan date to be very long ago and retry
3199 // This shouldn't fetch, but it should scan the local keychain
3200 self.silentFetchesAllowed = false;
3202 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
3203 NSError* error = nil;
3204 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
3205 XCTAssertNil(error, "no error pulling ckse from database");
3206 XCTAssertNotNil(ckse, "received a ckse");
3208 ckse.lastLocalKeychainScanTime = [NSDate distantPast];
3209 [ckse saveToDatabase:&error];
3210 XCTAssertNil(error, "no error saving to database");
3211 return CKKSDatabaseTransactionCommit;
3214 [self.keychainView xpc24HrNotification];
3215 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3216 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3218 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
3219 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3221 XCTAssertTrue(self.keychainView.initiatedLocalScan, "Should have initiated a local items scan due to 24-hr notification");
3222 self.keychainView.initiatedLocalScan = false;
3224 // And check that the fetch occurs as well
3225 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
3226 NSError* error = nil;
3227 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
3228 XCTAssertNil(error, "no error pulling ckse from database");
3229 XCTAssertNotNil(ckse, "received a ckse");
3231 ckse.lastFetchTime = [NSDate distantPast];
3232 [ckse saveToDatabase:&error];
3233 XCTAssertNil(error, "no error saving to database");
3234 return CKKSDatabaseTransactionCommit;
3237 [self expectCKFetch];
3238 [self.keychainView xpc24HrNotification];
3239 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3240 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3242 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
3243 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3245 XCTAssertFalse(self.keychainView.initiatedLocalScan, "Should not have initiated a local items scan due to 24-hr notification (if we've done one recently)");
3248 - (void)testRecoverFromZoneCreationFailure {
3249 // Fail the zone creation.
3250 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
3251 [self failNextZoneCreation:self.keychainZoneID];
3253 // Spin up CKKS subsystem.
3254 [self startCKKSSubsystem];
3256 // CKKS should figure it out, and fix it
3257 [self performOctagonTLKUpload:self.ckksViews];
3258 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3260 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
3261 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3262 [self addGenericPassword: @"data" account: @"account-delete-me"];
3263 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3265 XCTAssertNil(self.zones[self.keychainZoneID].creationError, "Creation error was unset (and so CKKS probably dealt with the error");
3268 - (void)testRecoverFromZoneSubscriptionFailure {
3269 // Fail the zone subscription.
3270 [self failNextZoneSubscription:self.keychainZoneID];
3272 // Spin up CKKS subsystem.
3273 [self startCKKSSubsystem];
3275 // The CKKS subsystem should figure out the issue, and fix it before Octagon uploads its items
3276 [self performOctagonTLKUpload:self.ckksViews];
3278 [self.keychainView waitForKeyHierarchyReadiness];
3279 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3281 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
3282 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3283 [self addGenericPassword: @"data" account: @"account-delete-me"];
3284 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3286 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
3289 - (void)testRecoverFromZoneSubscriptionFailureDueToZoneNotExisting {
3290 // This is different from testRecoverFromZoneSubscriptionFailure, since the zone is gone. CKKS must attempt to re-create the zone.
3292 // Silently fail the zone creation
3293 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
3294 [self failNextZoneCreationSilently:self.keychainZoneID];
3296 // Spin up CKKS subsystem.
3297 [self startCKKSSubsystem];
3299 // The CKKS subsystem should figure out the issue, and fix it.
3300 [self performOctagonTLKUpload:self.ckksViews];
3302 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3303 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3305 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
3306 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3307 [self addGenericPassword: @"data" account: @"account-delete-me"];
3308 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3310 XCTAssertFalse(self.zones[self.keychainZoneID].flag, "Zone flag was reset");
3311 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
3314 - (void)testRecoverFromDeletedTLKWithStashedTLK {
3315 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
3317 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3318 NSError* error = nil;
3321 [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
3322 XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
3324 // And delete the non-stashed version
3325 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
3326 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
3328 // Spin up CKKS subsystem.
3329 [self startCKKSSubsystem];
3331 [self.keychainView waitForKeyHierarchyReadiness];
3332 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3334 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3335 [self addGenericPassword: @"data" account: @"account-delete-me"];
3336 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3338 // CKKS should recreate the syncable TLK.
3339 [self checkNSyncableTLKsInKeychain: 1];
3342 - (void)testRecoverFromDeletedTLKWithStashedTLKUponRestart {
3343 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
3345 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3346 // Spin up CKKS subsystem.
3347 [self startCKKSSubsystem];
3348 [self.keychainView waitForKeyHierarchyReadiness];
3350 // Tear down the CKKS object
3351 [self.keychainView halt];
3353 NSError* error = nil;
3356 [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
3357 XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
3359 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
3360 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
3362 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
3363 [self beginSOSTrustedViewOperation:self.keychainView];
3364 [self.keychainView waitForKeyHierarchyReadiness];
3365 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3367 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3368 [self addGenericPassword: @"data" account: @"account-delete-me"];
3369 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3371 // CKKS should recreate the syncable TLK.
3372 [self checkNSyncableTLKsInKeychain: 1];
3376 // <rdar://problem/49024967> Octagon: tests for CK exceptions out of cuttlefish
3377 - (void)testRecoverFromTLKWriteFailure {
3378 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
3379 // Test starts with nothing in CloudKit, and will fail the first TLK write.
3380 NSError* noNetwork = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}];
3381 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:noNetwork];
3383 // Spin up CKKS subsystem.
3384 [self startCKKSSubsystem];
3386 // The CKKS subsystem should figure out the issue, and fix it.
3387 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3389 [self.keychainView waitForKeyHierarchyReadiness];
3390 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3392 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3393 [self addGenericPassword: @"data" account: @"account-delete-me"];
3394 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3396 // A network failure creating new TLKs shouldn't delete the 'failed' syncable one.
3397 [self checkNSyncableTLKsInKeychain: 2];
3401 // This test needs to be moved and rewritten now that Octagon handles TLK uploads
3402 // <rdar://problem/49024967> Octagon: tests for CK exceptions out of cuttlefish
3404 - (void)testRecoverFromTLKRace {
3405 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
3406 // Test starts with nothing in CloudKit, and will fail the first TLK write.
3407 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject: ^{
3408 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3411 // Spin up CKKS subsystem.
3412 [self startCKKSSubsystem];
3414 // The first TLK write should fail, and then our fake TLKs should be there in CloudKit.
3415 // It shouldn't write anything back up to CloudKit.
3416 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3418 // Now the TLKs arrive from the other device...
3419 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3420 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3421 [self.keychainView waitForKeyHierarchyReadiness];
3423 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3424 [self addGenericPassword: @"data" account: @"account-delete-me"];
3425 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3427 // A race failure creating new TLKs should delete the old syncable one.
3428 [self checkNSyncableTLKsInKeychain: 1];
3432 - (void)testRecoverFromNullCurrentKeyPointers {
3433 // The current key pointers in cloudkit shouldn't ever not exist if keys do. But, if they don't, CKKS must recover.
3435 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
3436 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3437 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3439 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
3440 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
3441 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
3442 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
3443 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
3445 // Spin up CKKS subsystem.
3446 [self startCKKSSubsystem];
3448 // The CKKS subsystem should figure out the issue, and fix it.
3449 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
3451 [self.keychainView waitForKeyHierarchyReadiness];
3453 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3456 - (void)testRecoverFromNoCurrentKeyPointers {
3457 // The current key pointers in cloudkit shouldn't ever point to nil. But, if they do, CKKS must recover.
3459 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
3460 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3461 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3463 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
3464 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentTLKPointer.storedCKRecord.recordID], "Deleted TLK pointer from zone");
3465 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassAPointer.storedCKRecord.recordID], "Deleted class a pointer from zone");
3466 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassCPointer.storedCKRecord.recordID], "Deleted class c pointer from zone");
3468 // Spin up CKKS subsystem.
3469 [self startCKKSSubsystem];
3471 // The CKKS subsystem should figure out the issue, and fix it.
3472 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
3474 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
3476 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3480 // <rdar://problem/49024967> Octagon: tests for CK exceptions out of cuttlefish
3481 - (void)testRecoverFromBadChangeTag {
3482 // 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.
3484 // Test starts with a broken key hierarchy in our fake CloudKit, but a (incorrectly) up-to-date change tag stored locally.
3485 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3486 SecCKKSTestSetDisableKeyNotifications(true); // Don't tell CKKS about this key material; we're pretending like this is a securityd restart
3487 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3488 SecCKKSTestSetDisableKeyNotifications(false);
3490 [self.keychainView dispatchSync: ^bool {
3491 NSError* error = nil;
3492 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainZoneID.zoneName];
3493 XCTAssertNotNil(ckse, "should have received a ckse");
3495 ckse.ckzonecreated = true;
3496 ckse.ckzonesubscribed = true;
3497 ckse.changeToken = self.keychainZone.currentChangeToken;
3499 [ckse saveToDatabase: &error];
3500 XCTAssertNil(error, "shouldn't have gotten an error saving to database");
3504 // The CKKS subsystem should try to write TLKs, but fail. It'll then upload a TLK share for the keys already in CloudKit
3505 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
3506 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3508 // Spin up CKKS subsystem.
3509 [self startCKKSSubsystem];
3510 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3512 // CKKS should then happily use the keys in CloudKit
3513 [self createClassCItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me"];
3514 [self createClassAItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me-class-a"];
3518 - (void)testRecoverFromDeletedKeysNewItem {
3519 [self startCKKSSubsystem];
3520 [self performOctagonTLKUpload:self.ckksViews];
3522 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3524 // We expect a single class C record to be uploaded.
3525 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
3526 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3528 [self addGenericPassword: @"data" account: @"account-delete-me"];
3529 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3531 [self waitForCKModifications];
3532 [self.keychainView waitUntilAllOperationsAreFinished];
3534 // Now, delete the local keys from the keychain (but leave the synced TLK)
3535 SecCKKSTestSetDisableKeyNotifications(true);
3536 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
3537 (id)kSecClass : (id)kSecClassInternetPassword,
3538 (id)kSecUseDataProtectionKeychain : @YES,
3539 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
3540 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
3541 }), @"Deleting local keys");
3542 SecCKKSTestSetDisableKeyNotifications(false);
3544 NSError* error = nil;
3545 [self.keychainZoneKeys.classC loadKeyMaterialFromKeychain:&error];
3546 XCTAssertNotNil(error, "Error loading class C key material from keychain");
3548 // We expect a single class C record to be uploaded.
3549 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
3550 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3552 [self addGenericPassword: @"datadata" account: @"account-no-keys"];
3553 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3555 // We expect a single class A record to be uploaded.
3556 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
3557 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3558 [self addGenericPassword:@"asdf"
3559 account:@"account-class-A"
3561 access:(id)kSecAttrAccessibleWhenUnlocked
3562 expecting:errSecSuccess
3563 message:@"Adding class A item"];
3564 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3567 - (void)testRecoverFromDeletedKeysReceive {
3568 [self startCKKSSubsystem];
3569 [self performOctagonTLKUpload:self.ckksViews];
3571 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at ready");
3573 [self waitForCKModifications];
3574 [self.keychainView waitUntilAllOperationsAreFinished];
3576 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
3577 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3579 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
3581 // Now, delete the local keys from the keychain (but leave the synced TLK)
3582 SecCKKSTestSetDisableKeyNotifications(true);
3583 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
3584 (id)kSecClass : (id)kSecClassInternetPassword,
3585 (id)kSecUseDataProtectionKeychain : @YES,
3586 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
3587 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
3588 }), @"Deleting local keys");
3589 SecCKKSTestSetDisableKeyNotifications(false);
3591 [self.keychainZone addToZone: ckr];
3592 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
3593 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3595 [self findGenericPassword: @"account0" expecting:errSecSuccess];
3598 - (void)testRecoverDeletedTLK {
3599 // If the TLK disappears halfway through, well, that's no good. But we should recover using TLK sharing
3601 [self startCKKSSubsystem];
3602 [self performOctagonTLKUpload:self.ckksViews];
3604 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
3606 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3607 [self waitForCKModifications];
3609 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
3610 [self.keychainView waitUntilAllOperationsAreFinished];
3612 // Now, delete the local keys from the keychain
3613 SecCKKSTestSetDisableKeyNotifications(true);
3614 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
3615 (id)kSecClass : (id)kSecClassInternetPassword,
3616 (id)kSecUseDataProtectionKeychain : @YES,
3617 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
3618 (id)kSecAttrSynchronizable : (id)kSecAttrSynchronizableAny,
3619 }), @"Deleting CKKS keys");
3620 SecCKKSTestSetDisableKeyNotifications(false);
3622 // Trigger a notification (with hilariously fake data)
3623 [self.keychainZone addToZone: ckr];
3625 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
3626 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3628 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should return to 'ready'");
3630 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
3631 [self.keychainView waitForFetchAndIncomingQueueProcessing]; // Do this again, to allow for non-atomic key state machinery switching
3633 [self findGenericPassword: @"account0" expecting:errSecSuccess];
3636 - (void)testRecoverMissingRolledKey {
3637 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3639 NSString* accountShouldExist = @"under-rolled-key";
3640 NSString* accountWillExist = @"under-rolled-key-later";
3641 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountShouldExist];
3642 [self.keychainZone addToZone: ckr];
3644 CKRecord* ckrAddedLater = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountWillExist];
3645 CKKSKey* pastClassCKey = self.keychainZoneKeys.classC;
3647 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3648 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3650 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3652 [self startCKKSSubsystem];
3653 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
3655 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3656 [self waitForCKModifications];
3658 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
3659 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
3660 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
3662 // Now, find and delete the class C key that ckrAddedLater is under
3663 NSError* error = nil;
3664 XCTAssertTrue([pastClassCKey deleteKeyMaterialFromKeychain:&error], "Should be able to delete old key material from keychain");
3665 XCTAssertNil(error, "Should be no error deleting old key material from keychain");
3667 [self.keychainZone addToZone:ckrAddedLater];
3668 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
3669 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3671 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
3672 [self findGenericPassword:accountWillExist expecting:errSecSuccess];
3674 XCTAssertTrue([pastClassCKey loadKeyMaterialFromKeychain:&error], "Class C key should be back in the keychain");
3675 XCTAssertNil(error, "Should be no error loading key from keychain");
3678 - (void)testRecoverMissingRolledClassAKeyWhileLocked {
3679 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3681 NSString* accountShouldExist = @"under-rolled-key";
3682 NSString* accountWillExist = @"under-rolled-key-later";
3683 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountShouldExist key:self.keychainZoneKeys.classA];
3684 [self.keychainZone addToZone: ckr];
3686 CKRecord* ckrAddedLater = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:accountWillExist key:self.keychainZoneKeys.classA];
3687 CKKSKey* pastClassAKey = self.keychainZoneKeys.classA;
3689 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3690 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3692 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3694 [self startCKKSSubsystem];
3695 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have returned to ready");
3697 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3698 [self waitForCKModifications];
3700 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
3701 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
3702 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
3704 // Now, find and delete the class C key that ckrAddedLater is under
3705 NSError* error = nil;
3706 XCTAssertTrue([pastClassAKey deleteKeyMaterialFromKeychain:&error], "Should be able to delete old key material from keychain");
3707 XCTAssertNil(error, "Should be no error deleting old key material from keychain");
3709 // now, lock the keychain
3710 self.aksLockState = true;
3711 [self.lockStateTracker recheck];
3713 [self.keychainZone addToZone:ckrAddedLater];
3714 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
3715 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3717 // Item should still not exist due to the lock state....
3718 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
3719 [self findGenericPassword:accountWillExist expecting:errSecItemNotFound];
3721 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], "Key state should have returned to readypendingunlock");
3723 self.aksLockState = false;
3724 [self.lockStateTracker recheck];
3727 [self.keychainView waitUntilAllOperationsAreFinished];
3728 [self findGenericPassword:accountShouldExist expecting:errSecSuccess];
3729 [self findGenericPassword:accountWillExist expecting:errSecSuccess];
3731 XCTAssertTrue([pastClassAKey loadKeyMaterialFromKeychain:&error], "Class A key should be back in the keychain");
3732 XCTAssertNil(error, "Should be no error loading key from keychain");
3735 - (void)testRecoverFromBadCurrentKeyPointer {
3736 // The current key pointers in cloudkit shouldn't ever point to missing entries. But, if they do, CKKS must recover.
3738 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
3739 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3740 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3742 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
3743 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
3744 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real tlk" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
3745 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class a key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
3746 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class c key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
3748 // Spin up CKKS subsystem.
3749 [self startCKKSSubsystem];
3751 // The CKKS subsystem should figure out the issue, and fix it (while uploading itself a TLK Share)
3752 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
3754 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
3756 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3759 - (void)testRecoverFromIncorrectCurrentTLKPointer {
3760 // The current key pointers in cloudkit shouldn't ever point to wrong entries. But, if they do, CKKS must recover.
3762 // Test starts with a rolled hierarchy, and CKPs pointing to the wrong items
3763 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3764 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3766 CKKSCurrentKeyPointer* oldTLKCKP = self.keychainZoneKeys.currentTLKPointer;
3767 CKRecord* oldTLKPointer = [self.keychainZone.currentDatabase[self.keychainZoneKeys.currentTLKPointer.storedCKRecord.recordID] copy];
3769 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3770 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3772 ZoneKeys* newZoneKeys = [self.keychainZoneKeys copy];
3774 // And put the oldTLKPointer back
3775 [self.zones[self.keychainZoneID] addToZone:oldTLKPointer];
3776 self.keychainZoneKeys.currentTLKPointer = oldTLKCKP;
3778 // Make sure it stuck:
3779 XCTAssertNotEqualObjects(self.keychainZoneKeys.currentTLKPointer,
3780 newZoneKeys.currentTLKPointer,
3781 "current TLK pointer should now not point to proper TLK");
3783 // Spin up CKKS subsystem.
3784 [self startCKKSSubsystem];
3786 // The CKKS subsystem should figure out the issue, and fix it (while uploading itself a TLK Share)
3787 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
3789 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
3791 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3792 [self waitForCKModifications];
3794 XCTAssertEqualObjects(self.keychainZoneKeys.currentTLKPointer,
3795 newZoneKeys.currentTLKPointer,
3796 "current TLK pointer should now point to proper TLK");
3797 XCTAssertEqualObjects(self.keychainZoneKeys.currentClassAPointer,
3798 newZoneKeys.currentClassAPointer,
3799 "current Class A pointer should now point to proper Class A key");
3800 XCTAssertEqualObjects(self.keychainZoneKeys.currentClassCPointer,
3801 newZoneKeys.currentClassCPointer,
3802 "current Class C pointer should now point to proper Class C key");
3805 - (void)testRecoverFromDesyncedKeyRecordsViaResync {
3806 // We need to set up a desynced situation to test our resync.
3807 // First, let CKKS start up and send several items to CloudKit (that we'll then desync!)
3808 __block NSError* error = nil;
3810 // Test starts with keys in CloudKit (so we can create items later)
3811 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3812 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3813 [self saveTLKMaterialToKeychain:self.keychainZoneID];
3815 [self addGenericPassword: @"data" account: @"first"];
3816 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
3818 [self startCKKSSubsystem];
3820 // Wait for uploads to happen
3821 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3822 [self waitForCKModifications];
3824 // Now, delete most of the key records are from on-disk, but the change token is not changed
3825 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
3826 CKKSCurrentKeySet* keyset = [CKKSCurrentKeySet loadForZone:self.keychainZoneID];
3828 XCTAssertNotNil(keyset.currentTLKPointer, @"should be a TLK pointer");
3829 XCTAssertNotNil(keyset.currentClassAPointer, @"should be a class A pointer");
3830 XCTAssertNotNil(keyset.currentClassCPointer, @"should be a class C pointer");
3832 [keyset.currentTLKPointer deleteFromDatabase:&error];
3833 XCTAssertNil(error, "Should be no error deleting TLK pointer from database");
3834 [keyset.currentClassAPointer deleteFromDatabase:&error];
3835 XCTAssertNil(error, "Should be no error deleting class A pointer from database");
3837 XCTAssertNotNil(keyset.tlk, @"should be a TLK");
3838 XCTAssertNotNil(keyset.classA, @"should be a classA key");
3839 XCTAssertNotNil(keyset.classC, @"should be a classC key");
3841 [keyset.tlk deleteFromDatabase:&error];
3842 XCTAssertNil(error, "Should be no error deleting TLK from database");
3844 [keyset.classA deleteFromDatabase:&error];
3845 XCTAssertNil(error, "Should be no error deleting classA from database");
3847 [keyset.classC deleteFromDatabase:&error];
3848 XCTAssertNil(error, "Should be no error deleting classC from database");
3850 return CKKSDatabaseTransactionCommit;
3853 // A restart should realize there's an issue, and pause for help
3854 // 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
3855 [self.keychainView halt];
3856 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
3857 [self.keychainView beginCloudKitOperation];
3858 [self beginSOSTrustedViewOperation:self.keychainView];
3860 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"key state should enter 'waitfortlkcreation'");
3862 // But, a resync should fix you back up
3863 CKKSSynchronizeOperation* resyncOperation = [self.keychainView resyncWithCloud];
3864 [resyncOperation waitUntilFinished];
3865 XCTAssertNil(resyncOperation.error, "No error during the resync operation");
3867 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
3870 - (void)testRecoverFromCloudKitFetchFail {
3871 // Test starts with nothing in database, but one in our fake CloudKit.
3872 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3873 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
3875 // The first two CKRecordZoneChanges should fail with a 'network unavailable' error.
3876 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
3877 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
3879 // Spin up CKKS subsystem.
3880 [self startCKKSSubsystem];
3882 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3883 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3884 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3886 // We expect a single record to be uploaded
3887 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3888 [self addGenericPassword: @"data" account: @"account-delete-me"];
3889 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3891 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
3892 [self addGenericPassword:@"asdf"
3893 account:@"account-class-A"
3895 access:(id)kSecAttrAccessibleWhenUnlocked
3896 expecting:errSecSuccess
3897 message:@"Adding class A item"];
3898 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3901 - (void)testRecoverFromCloudKitFetchNetworkFailAfterReady {
3902 // Test starts with nothing in database, but one in our fake CloudKit.
3903 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3905 // Spin up CKKS subsystem.
3906 [self startCKKSSubsystem];
3908 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
3910 // Network is unavailable
3911 [self.reachabilityTracker setNetworkReachability:false];
3913 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
3914 [self.keychainZone addToZone:ckr];
3916 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3918 // Say network is available
3919 [self.reachabilityTracker setNetworkReachability:true];
3921 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
3922 [self.keychainView waitForFetchAndIncomingQueueProcessing];
3924 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3927 - (void)testRecoverFromCloudKitFetchNetworkFailBeforeReady {
3928 // Test starts with nothing in database, but one in our fake CloudKit.
3929 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3931 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
3932 [self.keychainZone addToZone:ckr];
3934 // Network is unavailable
3935 [self.reachabilityTracker setNetworkReachability:false];
3937 // Spin up CKKS subsystem.
3938 [self startCKKSSubsystem];
3940 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateInitializing] wait:20*NSEC_PER_SEC], "CKKS entered initializing");
3942 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
3943 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
3944 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
3946 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
3948 // Say network is available
3949 [self.reachabilityTracker setNetworkReachability:true];
3951 [self.keychainView waitUntilAllOperationsAreFinished];
3952 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3954 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3957 - (void)testWaitAfterCloudKitNetworkFailDuringOutgoingQueueOperation {
3958 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
3960 [self startCKKSSubsystem];
3962 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered ready");
3964 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
3966 // Network is now unavailable
3967 [self.reachabilityTracker setNetworkReachability:false];
3969 NSError* noNetwork = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{
3970 CKErrorRetryAfterKey: @(0.2),
3972 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:noNetwork];
3973 [self addGenericPassword: @"data" account: @"account-delete-me"];
3975 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3978 // Once network is available again, the write should happen
3979 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
3980 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
3982 [self.reachabilityTracker setNetworkReachability:true];
3984 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
3986 OCMVerifyAllWithDelay(self.mockDatabase, 20);
3989 - (void)testRecoverFromCloudKitFetchFailWithDelay {
3990 // Test starts with nothing in database, but one in our fake CloudKit.
3991 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
3992 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
3994 // The first CKRecordZoneChanges should fail with a 'delay' error.
3995 self.silentFetchesAllowed = false;
3996 [self.keychainZone failNextFetchWith:[[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorRequestRateLimited userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:4]}]];
3997 [self expectCKFetch];
3999 // Spin up CKKS subsystem.
4000 [self startCKKSSubsystem];
4002 // Ensure it doesn't fetch within these three seconds (if it does, an exception will throw).
4005 // Okay, you can fetch again.
4006 [self expectCKFetch];
4008 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
4009 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
4010 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
4012 // We expect a single record to be uploaded
4013 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4014 [self addGenericPassword: @"data" account: @"account-delete-me"];
4015 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4018 - (void)testHandleZoneDeletedWhileFetching {
4019 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
4020 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
4021 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
4022 [self saveTLKMaterialToKeychain:self.keychainZoneID];
4024 // 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)
4025 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorZoneNotFound userInfo:@{}]];
4027 [self startCKKSSubsystem];
4029 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], @"Key state should become 'ready'");
4032 - (void)testRecoverFromCloudKitOldChangeToken {
4033 // Test starts with nothing in database, but one in our fake CloudKit.
4034 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
4036 // Spin up CKKS subsystem.
4037 [self startCKKSSubsystem];
4039 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
4040 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
4041 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
4043 // We expect a single record to be uploaded
4044 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4045 [self addGenericPassword: @"data" account: @"account-delete-me"];
4046 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4048 // Delete all old database states, to destroy the change tag validity
4049 [self.keychainZone.pastDatabases removeAllObjects];
4051 // We expect a total local flush and refetch
4052 self.silentFetchesAllowed = false;
4053 [self expectCKFetch]; // one to fail with a CKErrorChangeTokenExpired error
4054 [self expectCKFetch]; // and one to succeed
4056 // Trigger a fake change notification
4057 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
4059 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4061 // And check that a new upload happens just fine.
4062 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
4063 [self addGenericPassword:@"asdf"
4064 account:@"account-class-A"
4066 access:(id)kSecAttrAccessibleWhenUnlocked
4067 expecting:errSecSuccess
4068 message:@"Adding class A item"];
4069 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4072 - (void)testRecoverFromCloudKitUnknownDeviceStateRecord {
4073 // Test starts with nothing in database, but one in our fake CloudKit.
4074 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
4075 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
4076 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
4078 // Save a new device state record with some fake etag
4079 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
4080 CKKSDeviceStateEntry* cdse = [[CKKSDeviceStateEntry alloc] initForDevice:self.ckDeviceID
4081 osVersion:@"fake-record"
4082 lastUnlockTime:[NSDate date]
4085 circlePeerID:self.mockSOSAdapter.selfPeer.peerID
4086 circleStatus:kSOSCCInCircle
4087 keyState:SecCKKSZoneKeyStateWaitForTLK
4089 currentClassAUUID:nil
4090 currentClassCUUID:nil
4091 zoneID:self.keychainZoneID
4092 encodedCKRecord:nil];
4093 XCTAssertNotNil(cdse, "Should have created a fake CDSE");
4094 CKRecord* record = [cdse CKRecordWithZoneID:self.keychainZoneID];
4095 XCTAssertNotNil(record, "Should have created a fake CDSE CKRecord");
4096 record.etag = @"fake etag";
4097 cdse.storedCKRecord = record;
4099 NSError* error = nil;
4100 [cdse saveToDatabase:&error];
4101 XCTAssertNil(error, @"No error saving cdse to database");
4103 return CKKSDatabaseTransactionCommit;
4106 // Spin up CKKS subsystem.
4107 [self startCKKSSubsystem];
4109 // We expect a record failure, since the device state record is broke
4110 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
4112 // And then we expect a clean write
4113 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4114 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4116 [self addGenericPassword: @"data" account: @"account-delete-me"];
4117 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4120 - (void)testRecoverFromCloudKitUnknownItemRecord {
4121 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
4123 // Spin up CKKS subsystem.
4124 [self startCKKSSubsystem];
4126 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
4128 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
4129 [self.keychainZone addToZone:ckr];
4131 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
4132 [self.keychainView waitForFetchAndIncomingQueueProcessing];
4134 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
4136 // Delete the record from CloudKit, but miss the notification
4137 XCTAssertNil([self.keychainZone deleteCKRecordIDFromZone: ckr.recordID], "Deleting the record from fake CloudKit should succeed");
4139 // Expect a failed upload when we modify the item
4140 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
4141 [self updateGenericPassword:@"never seen again" account:@"account-delete-me"];
4142 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4144 [self.keychainView waitUntilAllOperationsAreFinished];
4146 // And the item should be disappeared from the local keychain
4147 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
4150 - (void)testRecoverFromCloudKitUserDeletedZone {
4151 // Test starts with nothing in database, but one in our fake CloudKit.
4152 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
4154 // Spin up CKKS subsystem.
4155 [self startCKKSSubsystem];
4157 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
4158 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
4159 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
4161 // We expect a single record to be uploaded
4162 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4163 [self addGenericPassword: @"data" account: @"account-delete-me"];
4164 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4166 // The first CKRecordZoneChanges should fail with a 'CKErrorUserDeletedZone' error. This will cause a local reset, ending up with zone re-creation.
4167 self.zones[self.keychainZoneID] = nil; // delete the zone
4168 self.keys[self.keychainZoneID] = nil;
4169 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorUserDeletedZone userInfo:@{}]];
4171 // We expect CKKS to recreate the zone, then have octagon reupload the keys, and then the class C item upload
4172 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4174 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
4176 [self performOctagonTLKUpload:self.ckksViews];
4178 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4180 // And check that a new upload occurs.
4181 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
4183 [self addGenericPassword:@"asdf"
4184 account:@"account-class-A"
4186 access:(id)kSecAttrAccessibleWhenUnlocked
4187 expecting:errSecSuccess
4188 message:@"Adding class A item"];
4189 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4192 - (void)testRecoverFromCloudKitZoneNotFoundWithoutZoneDeletion {
4193 // Test starts with nothing in database, but one in our fake CloudKit.
4194 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
4195 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
4197 // Spin up CKKS subsystem.
4198 [self startCKKSSubsystem];
4200 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
4201 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
4202 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
4204 // We expect a single record to be uploaded
4205 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4206 [self addGenericPassword: @"data" account: @"account-delete-me"];
4207 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4209 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
4211 [self waitForCKModifications];
4212 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
4214 // The next CKRecordZoneChanges will fail with a 'zone not found' error.
4215 self.zones[self.keychainZoneID] = nil; // delete the zone
4216 self.keys[self.keychainZoneID] = nil;
4218 // We expect CKKS to reset itself and recover, then have octagon upload the keys, and then the class C item upload
4219 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4221 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
4223 [self performOctagonTLKUpload:self.ckksViews];
4224 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4225 [self waitForCKModifications];
4227 // And check that a new upload occurs.
4228 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
4230 [self addGenericPassword:@"asdf"
4231 account:@"account-class-A"
4233 access:(id)kSecAttrAccessibleWhenUnlocked
4234 expecting:errSecSuccess
4235 message:@"Adding class A item"];
4236 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4239 - (void)testRecoverFromCloudKitZoneNotFoundFetchBeforeSigninOccurs {
4240 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
4242 // Before CKKS sign-in, it receives a fetch rpc
4243 XCTestExpectation *fetchReturns = [self expectationWithDescription:@"fetch returned"];
4244 [self.injectedManager rpcFetchAndProcessChanges:nil reply:^(NSError *result) {
4245 XCTAssertNil(result, "Should be no error fetching and processing changes");
4246 [fetchReturns fulfill];
4249 [self startCKKSSubsystem];
4251 [self performOctagonTLKUpload:self.ckksViews];
4252 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
4254 // We expect a single record to be uploaded
4255 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4256 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4257 [self addGenericPassword: @"data" account: @"account-delete-me"];
4258 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4260 // The fetch should have come back by now
4261 [self waitForExpectations: @[fetchReturns] timeout:5];
4264 - (void)testNoCloudKitAccount {
4265 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
4266 self.accountStatus = CKAccountStatusNoAccount;
4267 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4269 self.silentFetchesAllowed = false;
4270 [self startCKKSSubsystem];
4272 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4274 [self addGenericPassword: @"data" account: @"account-delete-me"];
4276 // simulate a NSNotification callback (but still logged out)
4277 self.accountStatus = CKAccountStatusNoAccount;
4278 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4280 // There should be no further uploads, even when we save keychain items
4281 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4282 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
4284 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4286 // Test that there are no items in the database (since we never logged in)
4287 [self checkNoCKKSData: self.keychainView];
4290 - (void)testSACloudKitAccount {
4291 // Test starts with nothing in database and the user logged into CloudKit and in circle, but the account is not HSA2.
4292 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4294 self.accountStatus = CKAccountStatusAvailable;
4296 self.silentFetchesAllowed = false;
4298 // Octagon does not initialize the ckks views when not in an HSA2 account
4299 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4300 [self startCKKSSubsystem];
4302 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4304 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4305 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForCloudKitAccountStatus] wait:20*NSEC_PER_SEC], "CKKS should enter 'waitforcloudkitaccount'");
4307 // There should be no uploads, even when we save keychain items and enter/exit circle
4308 [self addGenericPassword: @"data" account: @"account-delete-me"];
4310 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4311 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4312 [self endSOSTrustedOperationForAllViews];
4313 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4315 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4316 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4317 [self beginSOSTrustedViewOperation:self.keychainView];
4318 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
4320 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4322 // Test that there are no items in the database (since we never were in an HSA2 account)
4323 [self checkNoCKKSData: self.keychainView];
4326 - (void)testEarlyLogin
4328 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4330 // Octagon should initialize these views
4331 self.automaticallyBeginCKKSViewCloudKitOperation = true;
4333 self.accountStatus = CKAccountStatusAvailable;
4334 //[self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4336 [self startCKKSSubsystem];
4338 // CKKS should end up in 'waitfortlkcreation', as there's no trust and no TLKs
4339 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortlkcreation'");
4341 // Now, renotify the account status, and ensure that CKKS doesn't reenter 'initializing'
4342 CKKSCondition* initializing = self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateInitializing];
4344 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4346 XCTAssertNotEqual(0, [initializing wait:500*NSEC_PER_MSEC], "CKKS should not enter initializing when the device HSA status changes");
4349 - (void)testNoCircle {
4350 // Test starts with nothing in database and the user logged into CloudKit, but out of Circle.
4351 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4353 self.accountStatus = CKAccountStatusAvailable;
4355 [self startCKKSSubsystem];
4357 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4359 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4361 [self addGenericPassword: @"data" account: @"account-delete-me"];
4363 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortlkcreation'");
4365 // simulate a NSNotification callback (but still logged out)
4366 self.accountStatus = CKAccountStatusNoAccount;
4367 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4369 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'loggedout'");
4371 // There should be no further uploads, even when we save keychain items
4372 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4373 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
4375 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4377 // Test that there are no items in the database (since we never logged in)
4378 [self checkNoCKKSData: self.keychainView];
4381 - (void)testCircleDepartAndRejoin {
4382 // Test starts with CKKS in ready
4383 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
4384 [self startCKKSSubsystem];
4386 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
4387 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
4389 // But then, trust departs
4390 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4391 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4392 [self endSOSTrustedOperationForAllViews];
4394 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortrust'");
4396 // There should be no further uploads, even when we save keychain items
4397 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4398 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
4400 // Then trust returns. We expect two uploads
4401 [self expectCKModifyItemRecords:2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
4402 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4403 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4404 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4405 [self beginSOSTrustedViewOperation:self.keychainView];
4407 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
4408 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4411 - (void)testCloudKitLogin {
4412 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
4413 self.accountStatus = CKAccountStatusNoAccount;
4414 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4416 // Before we inform CKKS of its account state....
4417 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
4419 [self startCKKSSubsystem];
4421 XCTAssertEqual(0, [self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], "Should have been told of a 'logout' event on startup");
4422 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event shouldn't have happened");
4423 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4425 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4427 // simulate a cloudkit login and NSNotification callback
4428 self.accountStatus = CKAccountStatusAvailable;
4429 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4431 // No writes yet, since we're not in circle
4432 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortlkcreation'");
4434 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4435 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4436 [self beginSOSTrustedOperationForAllViews];
4438 [self performOctagonTLKUpload:self.ckksViews];
4440 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
4441 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
4442 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4444 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4445 [self waitForCKModifications];
4447 // We expect a single class C record to be uploaded.
4448 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4449 [self addGenericPassword: @"data" account: @"account-delete-me"];
4451 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4452 [self waitForCKModifications];
4455 - (void)testCloudKitLogoutLogin {
4456 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
4457 [self startCKKSSubsystem];
4458 [self performOctagonTLKUpload:self.ckksViews];
4459 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
4460 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
4461 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4463 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4464 [self waitForCKModifications];
4466 // We expect a single class C record to be uploaded.
4467 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4468 [self addGenericPassword: @"data" account: @"account-delete-me"];
4470 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4471 [self waitForCKModifications];
4473 // simulate a cloudkit logout and NSNotification callback
4474 self.accountStatus = CKAccountStatusNoAccount;
4475 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4476 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4477 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4478 [self endSOSTrustedOperationForAllViews];
4480 // Test that there are no items in the database after logout
4481 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
4482 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
4483 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4484 [self checkNoCKKSData: self.keychainView];
4486 // There should be no further uploads, even when we save keychain items
4487 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4488 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
4490 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
4491 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4492 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
4494 // simulate a cloudkit login
4495 // 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
4496 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
4498 self.accountStatus = CKAccountStatusAvailable;
4499 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4501 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4502 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4503 [self beginSOSTrustedViewOperation:self.keychainView];
4505 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
4506 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
4507 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4509 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4511 // Let everything settle...
4512 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
4513 [self waitForCKModifications];
4516 self.accountStatus = CKAccountStatusNoAccount;
4517 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4519 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4520 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4521 [self endSOSTrustedOperationForAllViews];
4523 // Test that there are no items in the database after logout
4524 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
4525 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
4526 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4527 [self checkNoCKKSData: self.keychainView];
4529 // There should be no further uploads, even when we save keychain items
4530 [self addGenericPassword: @"data" account: @"account-delete-me-5"];
4531 [self addGenericPassword: @"data" account: @"account-delete-me-6"];
4533 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
4534 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4536 // simulate a cloudkit login
4537 // 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
4538 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
4540 self.accountStatus = CKAccountStatusAvailable;
4541 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4543 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4544 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4545 [self beginSOSTrustedViewOperation:self.keychainView];
4547 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
4548 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
4549 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4551 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4553 // Let everything settle...
4554 [self.keychainView waitUntilAllOperationsAreFinished];
4555 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
4558 self.accountStatus = CKAccountStatusNoAccount;
4559 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4561 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4562 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4563 [self endSOSTrustedOperationForAllViews];
4565 // Test that there are no items in the database after logout
4566 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
4567 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
4568 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4569 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
4570 [self checkNoCKKSData: self.keychainView];
4572 // Force zone into error state
4573 OctagonStateTransitionOperation* transitionOp = [OctagonStateTransitionOperation named:@"enter" entering:SecCKKSZoneKeyStateError];
4574 OctagonStateTransitionRequest* request = [[OctagonStateTransitionRequest alloc] init:@"enter-wait-for-trust"
4575 sourceStates:[NSSet setWithArray:[CKKSZoneKeyStateMap() allKeys]]
4576 serialQueue:self.keychainView.queue
4577 timeout:10 * NSEC_PER_SEC
4578 transitionOp:transitionOp];
4579 [self.keychainView.stateMachine handleExternalRequest:request];
4580 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateError] wait:20*NSEC_PER_SEC], "CKKS entered 'error'");
4582 self.accountStatus = CKAccountStatusAvailable;
4583 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4585 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4586 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4587 [self beginSOSTrustedViewOperation:self.keychainView];
4589 XCTestExpectation *operationRun = [self expectationWithDescription:@"operation run"];
4590 NSOperation* op = [NSBlockOperation named:@"test" withBlock:^{
4591 [operationRun fulfill];
4594 [op addDependency:self.keychainView.keyStateReadyDependency];
4595 [self.operationQueue addOperation:op];
4597 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
4599 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
4600 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
4601 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4603 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4604 [self waitForExpectations: @[operationRun] timeout:10];
4605 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
4608 - (void)testCloudKitLogoutDueToGreyMode {
4609 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK shouldn't know the account state");
4610 [self startCKKSSubsystem];
4611 [self performOctagonTLKUpload:self.ckksViews];
4612 XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "Should have been told of a 'login'");
4613 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:50*NSEC_PER_MSEC], "'logout' event should be reset");
4614 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4616 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
4618 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4619 [self waitForCKModifications];
4621 // simulate a cloudkit grey mode switch and NSNotification callback. CKKS should treat this as a logout
4622 self.iCloudHasValidCredentials = false;
4623 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4625 // Test that there are no items in the database after logout
4626 XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "Should have been told of a 'logout'");
4627 XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:50*NSEC_PER_MSEC], "'login' event should be reset");
4628 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4629 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered 'logged out'");
4630 [self checkNoCKKSData:self.keychainView];
4632 // There should be no further uploads, even when we save keychain items
4633 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4634 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
4636 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
4637 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
4638 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4640 // Also, fetches shouldn't occur
4641 self.silentFetchesAllowed = false;
4642 NSOperation* op = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
4643 CKKSResultOperation* timeoutOp = [CKKSResultOperation named:@"timeout" withBlock:^{}];
4644 [timeoutOp addDependency:op];
4645 [timeoutOp timeout:4*NSEC_PER_SEC];
4646 [self.operationQueue addOperation:timeoutOp];
4647 [timeoutOp waitUntilFinished];
4649 // CloudKit figures its life out. We expect the two passwords from before to be uploaded
4650 [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
4651 self.silentFetchesAllowed = true;
4652 self.iCloudHasValidCredentials = true;
4653 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4655 XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "Should have been told of a 'login'");
4656 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:50*NSEC_PER_MSEC], "'logout' event should be reset");
4657 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4658 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4660 // And fetching still works!
4661 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D00" withAccount:@"account0"]];
4662 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
4663 [self.keychainView waitForFetchAndIncomingQueueProcessing];
4664 [self findGenericPassword: @"account0" expecting:errSecSuccess];
4665 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
4668 - (void)testCloudKitLoginRace {
4669 // Test starts with nothing in database, and 'in circle', but securityd hasn't received notification if we're logged into CloudKit.
4670 // CKKS should call handleLogout, as the CK account is not present.
4672 // note: don't unblock the ck account state object yet...
4674 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4675 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4677 // Add a keychain item, and make sure it doesn't upload yet.
4678 [self addGenericPassword: @"data" account: @"account-delete-me"];
4679 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForCloudKitAccountStatus] wait:20*NSEC_PER_SEC], "CKKS entered 'waitforcloudkitaccount'");
4680 XCTAssertNotEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:1*NSEC_PER_SEC], "CKKS shouldn't have entered 'waitforcloudkitaccount'");
4682 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4684 // Now that we're here (and logged out), bring the account up
4686 // We expect a single class C record to be uploaded.
4687 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4689 self.accountStatus = CKAccountStatusAvailable;
4690 [self startCKKSSubsystem];
4691 [self performOctagonTLKUpload:self.ckksViews];
4693 // simulate another NSNotification callback
4694 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4696 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4697 [self waitForCKModifications];
4699 // Make sure new items upload too
4700 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4701 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4702 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4704 [self.keychainView waitUntilAllOperationsAreFinished];
4705 [self waitForCKModifications];
4706 [self.keychainView halt];
4709 - (void)testDontLogOutIfBeforeFirstUnlock {
4711 // test starts as if a previously logged-in device has just rebooted
4712 self.aksLockState = true;
4713 self.accountStatus = CKAccountStatusAvailable;
4715 // This is the original state of the account tracker
4716 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCError error:nil];
4717 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4719 // And this is what the first circle status fetch will actually return
4720 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"]];
4721 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4723 XCTAssertEqual(self.accountStateTracker.currentComputedAccountStatus, CKKSAccountStatusUnknown, "Account tracker status should just be 'unknown'");
4724 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKKS should not yet know the CK account state");
4726 [self startCKKSSubsystem];
4728 XCTAssertEqual(0, [self.keychainView.loggedIn wait:8*NSEC_PER_SEC], "'login' event should have happened");
4729 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:10*NSEC_PER_MSEC], "Should not have been told of a CK 'logout' event on startup");
4730 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:1*NSEC_PER_SEC], "CKKS should know the account state");
4732 // And assume another CK status change
4733 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
4734 XCTAssertEqual(self.accountStateTracker.currentComputedAccountStatus, CKKSAccountStatusUnknown, "Account tracker status should just be 'no account'");
4735 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKKS should know the CK account state");
4737 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
4739 self.aksLockState = false;
4741 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4742 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4743 [self beginSOSTrustedViewOperation:self.keychainView];
4745 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
4746 XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
4747 XCTAssertEqual(0, [self.keychainView.accountStateKnown wait:50*NSEC_PER_MSEC], "CKK should know the account state");
4749 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4750 [self waitForCKModifications];
4752 // We expect a single class C record to be uploaded.
4753 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4754 [self addGenericPassword: @"data" account: @"account-delete-me"];
4756 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4757 [self waitForCKModifications];*/
4760 - (void)testSyncableItemsAddedWhileLoggedOut {
4761 // Test that once CKKS is up and 'logged out', nothing happens when syncable items are added
4762 self.accountStatus = CKAccountStatusNoAccount;
4763 [self startCKKSSubsystem];
4765 XCTAssertEqual([self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], 0, "CKKS should be told that it's logged out");
4767 // CKKS shouldn't decide to poke its state machine, but it should still send the notification
4768 XCTestExpectation* viewChangeNotification = [self expectChangeForView:self.keychainZoneID.zoneName];
4770 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4772 [self waitForExpectations:@[viewChangeNotification] timeout:8];
4775 - (void)testUploadSyncableItemsAddedWhileUntrusted {
4776 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
4777 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4779 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
4781 [self startCKKSSubsystem];
4783 XCTAssertEqual([self.keychainView.loggedIn wait:500*NSEC_PER_MSEC], 0, "CKKS should be told that it's logged in");
4785 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered waitfortrust");
4786 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4788 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4790 NSError* error = nil;
4791 NSDictionary* currentOQEs = [CKKSOutgoingQueueEntry countsByStateInZone:self.keychainZoneID error:&error];
4792 XCTAssertNil(error, "Should be no error counting OQEs");
4793 XCTAssertEqual(0, currentOQEs.count, "Should be no OQEs");
4795 // Now, insert a restart to simulate securityd restarting (and throwing away all pending operations), then a real sign in
4796 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4797 [self endSOSTrustedViewOperation:self.keychainView];
4798 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered waitfortrust");
4800 // Okay! Upon sign in, this item should be uploaded
4801 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4802 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4804 [self putSelfTLKSharesInCloudKit:self.keychainZoneID];
4805 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
4806 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
4807 [self beginSOSTrustedViewOperation:self.keychainView];
4809 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4810 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4813 - (void)testSyncableItemAddedOnDaemonRestartBeforePolicyLoaded {
4814 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
4815 [self startCKKSSubsystem];
4817 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4819 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
4820 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
4823 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4824 [self.injectedManager resetSyncingPolicy];
4825 [self.injectedManager haltZone:self.keychainZoneID.zoneName];
4827 // This item addition shouldn't be uploaded yet, or in any queues
4828 [self addGenericPassword:@"data" account:@"account-delete-me-2"];
4830 NSError* error = nil;
4831 NSDictionary* currentOQEs = [CKKSOutgoingQueueEntry countsByStateInZone:self.keychainZoneID error:&error];
4832 XCTAssertNil(error, "Should be no error counting OQEs");
4833 XCTAssertEqual(0, currentOQEs.count, "Should be no OQEs");
4835 [self.injectedManager setCurrentSyncingPolicy:self.viewSortingPolicyForManagedViewList];
4836 self.keychainView = [self.injectedManager findView:self.keychainZoneID.zoneName];
4837 // end of daemon restart
4839 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4840 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4842 [self.injectedManager beginCloudKitOperationOfAllViews];
4843 [self beginSOSTrustedViewOperation:self.keychainView];
4845 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4846 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4849 // Note that this test assumes that the keychainView object was created at daemon restart.
4850 // I don't really know how to write a test for that...
4851 - (void)testSyncableItemAddedOnDaemonRestartBeforeCloudKitAccountKnown {
4852 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
4853 [self startCKKSSubsystem];
4855 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4858 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4859 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4860 [self beginSOSTrustedViewOperation:self.keychainView];
4862 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4863 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:100*NSEC_PER_MSEC], "CKKS should still have no idea what the account state is");
4864 XCTAssertEqual(self.keychainView.accountStatus, CKKSAccountStatusUnknown, "Account status should be unknown");
4865 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForCloudKitAccountStatus] wait:20*NSEC_PER_SEC], "CKKS entered 'waitforcloudkitaccount'");
4867 [self.keychainView beginCloudKitOperation];
4869 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4870 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4871 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4872 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4875 - (void)testSyncableItemModifiedOnDaemonRestartBeforeCloudKitAccountKnown {
4876 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
4877 [self startCKKSSubsystem];
4879 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4881 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4882 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4883 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4884 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4887 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4888 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4889 [self beginSOSTrustedViewOperation:self.keychainView];
4891 [self updateGenericPassword:@"newdata" account: @"account-delete-me-2"];
4892 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:100*NSEC_PER_MSEC], "CKKS should still have no idea what the account state is");
4893 XCTAssertEqual(self.keychainView.accountStatus, CKKSAccountStatusUnknown, "Account status should be unknown");
4894 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForCloudKitAccountStatus] wait:20*NSEC_PER_SEC], "CKKS entered 'waitforcloudkitaccount'");
4896 [self.keychainView beginCloudKitOperation];
4898 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4899 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4900 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4901 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4904 - (void)testSyncableItemDeletedOnDaemonRestartBeforeCloudKitAccountKnown {
4905 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
4906 [self startCKKSSubsystem];
4908 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4910 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
4911 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
4912 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
4913 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4916 self.automaticallyBeginCKKSViewCloudKitOperation = false;
4917 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
4918 [self beginSOSTrustedViewOperation:self.keychainView];
4920 [self deleteGenericPassword:@"account-delete-me-2"];
4921 XCTAssertNotEqual(0, [self.keychainView.accountStateKnown wait:100*NSEC_PER_MSEC], "CKKS should still have no idea what the account state is");
4922 XCTAssertEqual(self.keychainView.accountStatus, CKKSAccountStatusUnknown, "Account status should be unknown");
4923 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForCloudKitAccountStatus] wait:20*NSEC_PER_SEC], "CKKS entered 'waitforcloudkitaccount'");
4925 [self.keychainView beginCloudKitOperation];
4927 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
4928 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
4929 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4932 - (void)testNotStuckAfterReset {
4933 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
4935 XCTestExpectation *operationRun = [self expectationWithDescription:@"operation run"];
4936 NSOperation* op = [NSBlockOperation named:@"test" withBlock:^{
4937 [operationRun fulfill];
4940 [op addDependency:self.keychainView.keyStateReadyDependency];
4941 [self.operationQueue addOperation:op];
4943 // And handle a spurious logout
4944 [self.keychainView handleCKLogout];
4946 [self startCKKSSubsystem];
4948 [self waitForExpectations: @[operationRun] timeout:20];
4949 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
4952 - (void)testCKKSControlBringup {
4953 NSXPCInterface *interface = CKKSSetupControlProtocol([NSXPCInterface interfaceWithProtocol:@protocol(CKKSControlProtocol)]);
4954 XCTAssertNotNil(interface, "Received a configured CKKS interface");
4957 - (void)testMetricsUpload {
4959 XCTestExpectation *upload = [self expectationWithDescription:@"CAMetrics"];
4960 XCTestExpectation *collection = [self expectationWithDescription:@"CAMetrics"];
4962 id saMock = OCMClassMock([SecCoreAnalytics class]);
4963 OCMStub([saMock sendEvent:[OCMArg any] event:[OCMArg any]]).andDo(^(NSInvocation* invocation) {
4967 NSString *sampleSampler = @"stuff";
4969 [[CKKSAnalytics logger] AddMultiSamplerForName:sampleSampler withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport block:^NSDictionary<NSString *,NSNumber *> *{
4970 [collection fulfill];
4971 return @{ @"hej" : @1 };
4975 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
4976 [self startCKKSSubsystem];
4978 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
4980 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
4981 deletedRecordTypeCounts:nil
4982 zoneID:self.keychainZoneID
4983 checkModifiedRecord:nil
4984 runAfterModification:nil];
4986 [self.injectedManager xpc24HrNotification];
4988 [self waitForExpectations: @[upload, collection] timeout:10];
4989 [[CKKSAnalytics logger] removeMultiSamplerForName:sampleSampler];
4992 - (void)testSaveManyTLKShares {
4993 // Spin up CKKS subsystem.
4994 [self startCKKSSubsystem];
4996 [self performOctagonTLKUpload:self.ckksViews];
4997 OCMVerifyAllWithDelay(self.mockDatabase, 20);
4999 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
5001 NSMutableArray<CKKSSOSSelfPeer*>* peers = [NSMutableArray array];
5003 for(int i = 0; i < 20; i++) {
5004 CKKSSOSSelfPeer* untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:[NSString stringWithFormat:@"untrusted-peer-%d", i]
5005 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
5006 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
5007 viewList:self.managedViewList];
5009 [peers addObject:untrustedPeer];
5012 NSMutableArray<CKRecord*>* tlkShareRecords = [NSMutableArray array];
5014 for(CKKSSOSSelfPeer* peer1 in peers) {
5015 for(CKKSSOSSelfPeer* peer2 in peers) {
5016 NSError* error = nil;
5017 CKKSTLKShareRecord* share = [CKKSTLKShareRecord share:self.keychainZoneKeys.tlk
5023 XCTAssertNil(error, "Should have been no error sharing a CKKSKey");
5024 XCTAssertNotNil(share, "Should be able to create a share");
5026 CKRecord* shareRecord = [share CKRecordWithZoneID:self.keychainZoneID];
5027 [tlkShareRecords addObject:shareRecord];
5031 [self measureBlock:^{
5032 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
5033 for(CKRecord* record in tlkShareRecords) {
5034 [self.keychainView _onqueueCKRecordChanged:record resync:false];
5036 return CKKSDatabaseTransactionCommit;
5041 - (void)testReceiveNotificationDuringLaunch {
5042 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
5044 [self holdCloudKitModifyRecordZones];
5046 // Spin up CKKS subsystem.
5047 [self startCKKSSubsystem];
5049 CKKSCondition* fetcherCondition = self.keychainView.zoneChangeFetcher.fetchScheduler.liveRequestReceived;
5051 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
5053 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
5055 XCTAssertNotEqual(0, [fetcherCondition wait:(3 * NSEC_PER_SEC)], "not supposed to get a fetch data");
5057 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
5058 self.silentFetchesAllowed = false;
5059 [self expectCKFetch];
5060 [self releaseCloudKitModifyRecordZonesHold];
5062 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
5063 OCMVerifyAllWithDelay(self.mockDatabase, 20);