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>
34 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
35 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
36 #import "keychain/ckks/CKKS.h"
37 #import "keychain/ckks/CKKSControlProtocol.h"
38 #import "keychain/ckks/CKKSCurrentKeyPointer.h"
39 #import "keychain/ckks/CKKSItemEncrypter.h"
40 #import "keychain/ckks/CKKSKey.h"
41 #import "keychain/ckks/CKKSOutgoingQueueEntry.h"
42 #import "keychain/ckks/CKKSIncomingQueueEntry.h"
43 #import "keychain/ckks/CKKSSynchronizeOperation.h"
44 #import "keychain/ckks/CKKSViewManager.h"
45 #import "keychain/ckks/CKKSZoneStateEntry.h"
46 #import "keychain/ckks/CKKSManifest.h"
47 #import "keychain/ckks/CKKSAnalyticsLogger.h"
48 #import "keychain/ckks/CKKSHealKeyHierarchyOperation.h"
49 #import "keychain/ckks/CKKSZoneChangeFetcher.h"
51 #import "keychain/ckks/tests/MockCloudKit.h"
53 #import "keychain/ckks/tests/CKKSTests.h"
55 @implementation CloudKitKeychainSyncingTestsBase
57 - (ZoneKeys*)keychainZoneKeys {
58 return self.keys[self.keychainZoneID];
61 // Override our base class
62 -(NSSet*)managedViewList {
63 return [NSSet setWithObject:@"keychain"];
68 SecCKKSResetSyncing();
75 self.keychainZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"keychain" ownerName:CKCurrentUserDefaultName];
76 self.keychainZone = [[FakeCKZone alloc] initZone: self.keychainZoneID];
78 SFECKeyPair* keyPair = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
79 [CKKSManifestInjectionPointHelper registerEgoPeerID:@"MeMyselfAndI" keyPair:keyPair];
81 // Wait for the ViewManager to be brought up
82 XCTAssertEqual(0, [self.injectedManager.completedSecCKKSInitialize wait:4*NSEC_PER_SEC], "No timeout waiting for SecCKKSInitialize");
84 self.keychainView = [[CKKSViewManager manager] findView:@"keychain"];
85 XCTAssertNotNil(self.keychainView, "CKKSViewManager created the keychain view");
87 // Check that your environment is set up correctly
88 XCTAssertFalse([CKKSManifest shouldSyncManifests], "Manifests syncing is disabled");
89 XCTAssertFalse([CKKSManifest shouldEnforceManifests], "Manifests enforcement is disabled");
94 SecCKKSResetSyncing();
98 // Fetch status, to make sure we can
99 NSDictionary* status = [self.keychainView status];
102 self.keychainView = nil;
103 self.keychainZoneID = nil;
108 - (FakeCKZone*)keychainZone {
109 return self.zones[self.keychainZoneID];
112 - (void)setKeychainZone: (FakeCKZone*) zone {
113 self.zones[self.keychainZoneID] = zone;
118 @implementation CloudKitKeychainSyncingTests
122 - (void)testAddItem {
123 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
125 // We expect a single record to be uploaded.
126 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
128 [self startCKKSSubsystem];
130 [self addGenericPassword: @"data" account: @"account-delete-me"];
132 OCMVerifyAllWithDelay(self.mockDatabase, 8);
135 - (void)testActiveTLKS {
136 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
138 // We expect a single record to be uploaded.
139 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
141 [self startCKKSSubsystem];
143 [self addGenericPassword: @"data" account: @"account-delete-me"];
145 OCMVerifyAllWithDelay(self.mockDatabase, 8);
147 NSDictionary<NSString *,NSString *>* tlks = [[CKKSViewManager manager] activeTLKs];
149 XCTAssertEqual([tlks count], (NSUInteger)1, "One TLK");
150 XCTAssertNotNil(tlks[@"keychain"], "keychain have a UUID");
154 - (void)testAddMultipleItems {
155 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
156 [self startCKKSSubsystem];
158 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
159 [self addGenericPassword: @"data" account: @"account-delete-me"];
160 OCMVerifyAllWithDelay(self.mockDatabase, 8);
162 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
163 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
164 OCMVerifyAllWithDelay(self.mockDatabase, 8);
166 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
167 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
168 OCMVerifyAllWithDelay(self.mockDatabase, 8);
171 - (void)testAddItemWithoutUUID {
172 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
173 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
174 [self saveTLKMaterialToKeychain:self.keychainZoneID];
176 [self startCKKSSubsystem];
178 [self.keychainView waitUntilAllOperationsAreFinished];
180 SecCKKSTestSetDisableAutomaticUUID(true);
181 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
183 SecCKKSTestSetDisableAutomaticUUID(false);
185 // We then expect an upload of the added item
186 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
188 OCMVerifyAllWithDelay(self.mockDatabase, 8);
191 - (void)testModifyItem {
192 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
194 NSString* account = @"account-delete-me";
196 [self startCKKSSubsystem];
198 // We expect a single record to be uploaded.
199 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
200 [self addGenericPassword: @"data" account: account];
201 OCMVerifyAllWithDelay(self.mockDatabase, 8);
203 // And then modified.
204 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
205 [self updateGenericPassword: @"otherdata" account:account];
206 OCMVerifyAllWithDelay(self.mockDatabase, 8);
209 - (void)testModifyItemImmediately {
210 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
211 NSString* account = @"account-delete-me";
213 [self startCKKSSubsystem];
214 [self holdCloudKitModifications];
216 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
217 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
218 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
219 [self addGenericPassword: @"data" account: account];
220 OCMVerifyAllWithDelay(self.mockDatabase, 8);
222 // Right now, the write in CloudKit is pending. Make the local modification...
223 [self updateGenericPassword: @"otherdata" account:account];
225 // And then schedule the update
226 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
227 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
228 [self releaseCloudKitModificationHold];
230 OCMVerifyAllWithDelay(self.mockDatabase, 8);
233 - (void)testModifyItemPrimaryKey {
234 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
236 NSString* account = @"account-delete-me";
238 [self startCKKSSubsystem];
240 // We expect a single record to be uploaded.
241 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
242 [self addGenericPassword: @"data" account: account];
243 OCMVerifyAllWithDelay(self.mockDatabase, 8);
245 // And then modified. Since we're changing the "primary key", we expect to delete the old record and upload a new one.
246 [self expectCKModifyItemRecords:1 deletedRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:nil];
247 [self updateAccountOfGenericPassword: @"new-account-delete-me" account:account];
248 OCMVerifyAllWithDelay(self.mockDatabase, 8);
251 - (void)testModifyItemDuringReencrypt {
252 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
253 NSString* account = @"account-delete-me";
255 [self startCKKSSubsystem];
256 [self holdCloudKitModifications];
258 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
259 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
260 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
261 [self addGenericPassword: @"data" account: account];
262 OCMVerifyAllWithDelay(self.mockDatabase, 8);
264 // Right now, the write in CloudKit is pending. Make the local modification...
265 [self updateGenericPassword: @"otherdata" account:account];
267 // And then schedule the update, but for the final version of the password
268 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
269 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"third"]];
271 // Stop the reencrypt operation from happening
272 self.keychainView.holdReencryptOutgoingItemsOperation = [CKKSGroupOperation named:@"reencrypt-hold" withBlock: ^{
273 secnotice("ckks", "releasing reencryption hold");
276 // The cloudkit operation finishes, letting the next OQO proceed (and set up the reencryption operation)
277 [self releaseCloudKitModificationHold];
279 // And wait for this to finish...
280 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
281 // And once more to quiesce.
282 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
284 // Pause outgoing queue operations to ensure the reencryption operation runs first
285 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
286 secnotice("ckks", "releasing outgoing-queue hold");
289 [self updateGenericPassword: @"third" account:account];
291 // Run the reencrypt items operation to completion.
292 [self.operationQueue addOperation: self.keychainView.holdReencryptOutgoingItemsOperation];
293 [self.keychainView waitForOperationsOfClass:[CKKSReencryptOutgoingItemsOperation class]];
295 [self.operationQueue addOperation: self.keychainView.holdOutgoingQueueOperation];
297 OCMVerifyAllWithDelay(self.mockDatabase, 8);
298 [self.keychainView waitUntilAllOperationsAreFinished];
299 [self waitForCKModifications];
302 - (void)testModifyItemBeforeReencrypt {
303 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
304 NSString* account = @"account-delete-me";
306 [self startCKKSSubsystem];
307 [self holdCloudKitModifications];
309 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
310 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
311 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
312 [self addGenericPassword: @"data" account: account];
313 OCMVerifyAllWithDelay(self.mockDatabase, 8);
315 // Right now, the write in CloudKit is pending. Make the local modification...
316 [self updateGenericPassword: @"otherdata" account:account];
318 // And then schedule the update, but for the final version of the password
319 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
320 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"third"]];
322 // Stop the reencrypt operation from happening
323 self.keychainView.holdReencryptOutgoingItemsOperation = [CKKSGroupOperation named:@"reencrypt-hold" withBlock: ^{
324 secnotice("ckks", "releasing reencryption hold");
327 // The cloudkit operation finishes, letting the next OQO proceed (and set up the reencryption operation)
328 [self releaseCloudKitModificationHold];
330 // And wait for this to finish...
331 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
332 // And once more to quiesce.
333 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
335 [self updateGenericPassword: @"third" account:account];
337 // Item should upload.
338 OCMVerifyAllWithDelay(self.mockDatabase, 8);
340 // Run the reencrypt items operation to completion.
341 [self.operationQueue addOperation: self.keychainView.holdReencryptOutgoingItemsOperation];
342 [self.keychainView waitForOperationsOfClass:[CKKSReencryptOutgoingItemsOperation class]];
344 [self.keychainView waitUntilAllOperationsAreFinished];
345 [self waitForCKModifications];
348 - (void)testModifyItemDuringNetworkFailure {
349 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
350 NSString* account = @"account-delete-me";
352 [self startCKKSSubsystem];
353 [self holdCloudKitModifications];
355 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
356 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
358 [self addGenericPassword: @"data" account: account];
359 OCMVerifyAllWithDelay(self.mockDatabase, 8);
361 // Right now, the write in CloudKit is pending. Make the local modification...
362 [self updateGenericPassword: @"otherdata" account:account];
364 // And then schedule the update, but for the final version of the password
365 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
366 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
368 // The cloudkit operation finishes, letting the next OQO proceed (and set up uploading the new item)
369 [self releaseCloudKitModificationHold];
371 // Item should upload.
372 OCMVerifyAllWithDelay(self.mockDatabase, 8);
374 [self.keychainView waitUntilAllOperationsAreFinished];
375 [self waitForCKModifications];
378 - (void)testDeleteItem {
379 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
381 [self startCKKSSubsystem];
383 // We expect a single record to be uploaded.
384 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
385 [self addGenericPassword: @"data" account: @"account-delete-me"];
386 OCMVerifyAllWithDelay(self.mockDatabase, 8);
388 // We expect a single record to be deleted.
389 [self expectCKDeleteItemRecords: 1 zoneID:self.keychainZoneID];
390 [self deleteGenericPassword:@"account-delete-me"];
391 OCMVerifyAllWithDelay(self.mockDatabase, 8);
394 - (void)testDeleteItemImmediatelyAfterModify {
395 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
396 NSString* account = @"account-delete-me";
398 [self startCKKSSubsystem];
400 // We expect a single record to be uploaded.
401 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
402 [self addGenericPassword: @"data" account: account];
403 OCMVerifyAllWithDelay(self.mockDatabase, 8);
405 // Now, hold the modify
406 [self holdCloudKitModifications];
408 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
409 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
410 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
412 [self updateGenericPassword: @"otherdata" account:account];
413 OCMVerifyAllWithDelay(self.mockDatabase, 8);
415 // Right now, the write in CloudKit is pending. Make the local deletion...
416 [self deleteGenericPassword:account];
418 // And then schedule the update
419 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
420 [self releaseCloudKitModificationHold];
422 OCMVerifyAllWithDelay(self.mockDatabase, 8);
425 - (void)testDeleteItemAfterFetchAfterModify {
426 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
427 NSString* account = @"account-delete-me";
429 [self startCKKSSubsystem];
431 // We expect a single record to be uploaded.
432 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
433 [self addGenericPassword: @"data" account: account];
434 OCMVerifyAllWithDelay(self.mockDatabase, 8);
436 // Now, hold the modify
437 //[self holdCloudKitModifications];
439 // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
440 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
441 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
443 [self updateGenericPassword: @"otherdata" account:account];
444 OCMVerifyAllWithDelay(self.mockDatabase, 8);
446 // Right now, the write in CloudKit is pending. Place a hold on outgoing queue processing
447 // Place a hold on processing the outgoing queue.
448 CKKSResultOperation* blockOutgoing = [CKKSResultOperation operationWithBlock:^{
449 secnotice("ckks", "Outgoing queue hold released.");
451 blockOutgoing.name = @"outgoing-queue-hold";
452 CKKSResultOperation* outgoingQueueOperation = [self.keychainView processOutgoingQueueAfter:blockOutgoing ckoperationGroup:nil];
454 [self deleteGenericPassword:account];
456 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
458 // Release the CK modification hold
459 //[self releaseCloudKitModificationHold];
462 [self.keychainView waitForFetchAndIncomingQueueProcessing];
463 [self.operationQueue addOperation:blockOutgoing];
464 [outgoingQueueOperation waitUntilFinished];
466 OCMVerifyAllWithDelay(self.mockDatabase, 8);
470 - (void)testReceiveItem {
471 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
472 [self startCKKSSubsystem];
474 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
475 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
476 (id)kSecAttrAccount : @"account-delete-me",
477 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
478 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
481 CFTypeRef item = NULL;
482 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
484 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
485 [self.keychainZone addToZone: ckr];
487 // Trigger a notification (with hilariously fake data)
488 [self.keychainView notifyZoneChange:nil];
490 [self.keychainView waitForFetchAndIncomingQueueProcessing];
491 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
494 - (void)testReceiveManyItems {
495 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
496 [self startCKKSSubsystem];
498 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D00" withAccount:@"account0"]];
499 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D01" withAccount:@"account1"]];
500 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D02" withAccount:@"account2"]];
501 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D03" withAccount:@"account3"]];
502 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D04" withAccount:@"account4"]];
503 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D05" withAccount:@"account5"]];
504 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D06" withAccount:@"account6"]];
505 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D07" withAccount:@"account7"]];
506 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D08" withAccount:@"account8"]];
507 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D09" withAccount:@"account9"]];
508 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D10" withAccount:@"account10"]];
509 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D11" withAccount:@"account11"]];
511 // Trigger a notification (with hilariously fake data)
512 [self.keychainView notifyZoneChange:nil];
514 [self.keychainView waitForFetchAndIncomingQueueProcessing];
516 [self findGenericPassword: @"account0" expecting:errSecSuccess];
517 [self findGenericPassword: @"account1" expecting:errSecSuccess];
518 [self findGenericPassword: @"account2" expecting:errSecSuccess];
519 [self findGenericPassword: @"account3" expecting:errSecSuccess];
520 [self findGenericPassword: @"account4" expecting:errSecSuccess];
521 [self findGenericPassword: @"account5" expecting:errSecSuccess];
522 [self findGenericPassword: @"account6" expecting:errSecSuccess];
523 [self findGenericPassword: @"account7" expecting:errSecSuccess];
524 [self findGenericPassword: @"account8" expecting:errSecSuccess];
525 [self findGenericPassword: @"account9" expecting:errSecSuccess];
526 [self findGenericPassword: @"account10" expecting:errSecSuccess];
527 [self findGenericPassword: @"account11" expecting:errSecSuccess];
530 - (void)testReceiveCollidingItem {
531 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
532 [self startCKKSSubsystem];
534 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
535 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
536 (id)kSecAttrAccount : @"account-delete-me",
537 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
538 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
541 CFTypeRef item = NULL;
542 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
544 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
545 CKRecord* ckr2 = [self createFakeRecord: self.keychainZoneID recordName: @"F9C58D31-7B59-481E-98AC-5A507ACB2D85"];
547 [self.keychainZone addToZone: ckr];
548 [self.keychainZone addToZone: ckr2];
550 // We expect a delete operation with the "higher" UUID.
551 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
553 // Trigger a notification (with hilariously fake data)
554 [self.keychainView notifyZoneChange:nil];
556 OCMVerifyAllWithDelay(self.mockDatabase, 6);
557 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
559 [self waitForCKModifications];
560 XCTAssertNil(self.keychainZone.currentDatabase[ckr2.recordID], "Correct record was deleted from CloudKit");
563 -(void)testReceiveItemDelete {
564 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
565 [self startCKKSSubsystem];
567 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
568 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
569 (id)kSecAttrAccount : @"account-delete-me",
570 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
571 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
574 CFTypeRef item = NULL;
575 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
577 [self.keychainView waitForFetchAndIncomingQueueProcessing];
579 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
580 [self.keychainZone addToZone: ckr];
582 // Trigger a notification (with hilariously fake data)
583 [self.keychainView notifyZoneChange:nil];
584 [self.keychainView waitForFetchAndIncomingQueueProcessing];
586 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
590 [self.keychainZone deleteCKRecordIDFromZone: [ckr recordID]];
591 [self.keychainView notifyZoneChange:nil];
592 [self.keychainView waitForFetchAndIncomingQueueProcessing];
594 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
597 -(void)testReceiveItemPhantomDelete {
598 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
599 [self startCKKSSubsystem];
601 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
602 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
603 (id)kSecAttrAccount : @"account-delete-me",
604 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
605 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
608 CFTypeRef item = NULL;
609 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
611 [self.keychainView waitForFetchAndIncomingQueueProcessing];
613 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName: @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
614 [self.keychainZone addToZone: ckr];
616 // Trigger a notification (with hilariously fake data)
617 [self.keychainView notifyZoneChange:nil];
618 [self.keychainView waitForFetchAndIncomingQueueProcessing];
620 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should exist now");
623 [self.keychainView waitUntilAllOperationsAreFinished];
626 [self.keychainZone deleteCKRecordIDFromZone: [ckr recordID]];
628 // and add another, incorrect IQE
629 [self.keychainView dispatchSync: ^bool {
630 // Inefficient, but hey, it works
631 CKRecord* record = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-FFFF-FFFF-5A507ACB2D85"];
632 CKKSItem* fakeItem = [[CKKSItem alloc] initWithCKRecord: record];
634 CKKSIncomingQueueEntry* iqe = [[CKKSIncomingQueueEntry alloc] initWithCKKSItem:fakeItem
635 action:SecCKKSActionDelete
636 state:SecCKKSStateNew];
637 XCTAssertNotNil(iqe, "could create fake IQE");
638 NSError* error = nil;
639 XCTAssert([iqe saveToDatabase: &error], "Saved fake IQE to database");
640 XCTAssertNil(error, "No error saving fake IQE to database");
644 [self.keychainView notifyZoneChange:nil];
645 [self.keychainView waitForFetchAndIncomingQueueProcessing];
647 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
649 // The incoming queue should be empty
650 [self.keychainView dispatchSync: ^bool {
651 NSError* error = nil;
652 NSArray* iqes = [CKKSIncomingQueueEntry all:&error];
653 XCTAssertNil(error, "No error loading IQEs");
654 XCTAssertNotNil(iqes, "Could load IQEs");
655 XCTAssertEqual(iqes.count, 0u, "Incoming queue is empty");
659 -(void)testReceiveConflictOnJustAddedItem {
660 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
661 [self startCKKSSubsystem];
663 [self.keychainView waitUntilAllOperationsAreFinished];
665 // Place a hold on processing the outgoing queue.
666 CKKSResultOperation* blockOutgoing = [CKKSResultOperation operationWithBlock:^{
667 secnotice("ckks", "Outgoing queue hold released.");
669 blockOutgoing.name = @"outgoing-queue-hold";
670 CKKSResultOperation* outgoingQueueOperation = [self.keychainView processOutgoingQueueAfter:blockOutgoing ckoperationGroup:nil];
672 CKKSResultOperation* blockIncoming = [CKKSResultOperation operationWithBlock:^{
673 secnotice("ckks", "Incoming queue hold released.");
675 blockIncoming.name = @"incoming-queue-hold";
676 CKKSResultOperation* incomingQueueOperation = [self.keychainView processIncomingQueue:false after: blockIncoming];
678 [self addGenericPassword:@"localchange" account:@"account-delete-me"];
680 // Pull out the new item's UUID.
681 __block NSString* itemUUID = nil;
682 [self.keychainView dispatchSync:^bool {
683 NSError* error = nil;
684 NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:&error];
685 XCTAssertNil(error, "no error fetching uuids");
686 XCTAssertEqual(uuids.count, 1u, "There's exactly one outgoing queue entry");
689 XCTAssertNotNil(itemUUID, "Have a UUID for our new item");
693 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName: itemUUID]];
695 [self.keychainView notifyZoneChange:nil];
696 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
698 // Allow the outgoing queue operation to proceed
699 [self.operationQueue addOperation:blockOutgoing];
700 [outgoingQueueOperation waitUntilFinished];
702 // Allow the incoming queue operation to proceed
703 [self.operationQueue addOperation:blockIncoming];
704 [incomingQueueOperation waitUntilFinished];
706 [self checkGenericPassword:@"data" account:@"account-delete-me"];
708 [self.keychainView waitUntilAllOperationsAreFinished];
711 -(void)testReceiveUnknownField {
712 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
714 [self startCKKSSubsystem];
715 [self.keychainView waitForKeyHierarchyReadiness];
717 NSError* error = nil;
719 // Manually encrypt an item
720 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
721 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
722 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
723 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
724 parentKeyUUID:self.keychainZoneKeys.classA.uuid
725 zoneID:recordID.zoneID];
726 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classA error:&error];
727 XCTAssertNotNil(itemkey, "Got a key");
728 cipheritem.wrappedkey = itemkey.wrappedkey;
729 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
731 NSData* future_data_field = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding];
732 NSString* future_string_field = @"authstring";
733 NSString* future_server_field = @"server_can_change_at_any_time";
734 NSNumber* future_number_field = [NSNumber numberWithInt:30];
736 // Use version 2, so future fields will be authenticated
737 cipheritem.encver = CKKSItemEncryptionVersion2;
738 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
740 authenticatedData[@"future_data_field"] = future_data_field;
741 authenticatedData[@"future_string_field"] = [future_string_field dataUsingEncoding:NSUTF8StringEncoding];
743 uint64_t n = OSSwapHostToLittleConstInt64([future_number_field unsignedLongValue]);
744 authenticatedData[@"future_number_field"] = [NSData dataWithBytes:&n length:sizeof(n)];
747 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
748 XCTAssertNil(error, "no error encrypting object");
749 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
751 CKRecord* ckr = [cipheritem CKRecordWithZoneID: recordID.zoneID];
752 ckr[@"future_data_field"] = future_data_field;
753 ckr[@"future_string_field"] = future_string_field;
754 ckr[@"future_number_field"] = future_number_field;
755 ckr[@"server_new_server_field"] = future_server_field;
756 [self.keychainZone addToZone:ckr];
758 [self.keychainView waitForFetchAndIncomingQueueProcessing];
760 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
761 (id)kSecReturnAttributes: @YES,
762 (id)kSecAttrSynchronizable: @YES,
763 (id)kSecAttrAccount: @"account-delete-me",
764 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
766 CFTypeRef cfresult = NULL;
767 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
769 // Test that if this item is updated, it remains encrypted in v2, and future_field still exists
770 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
771 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
773 OCMVerifyAllWithDelay(self.mockDatabase, 8);
774 [self waitForCKModifications];
776 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
777 XCTAssertEqualObjects(newRecord[@"future_data_field"], future_data_field, "future_data_field still exists");
778 XCTAssertEqualObjects(newRecord[@"future_string_field"], future_string_field, "future_string_field still exists");
779 XCTAssertEqualObjects(newRecord[@"future_number_field"], future_number_field, "future_string_field still exists");
780 XCTAssertEqualObjects(newRecord[@"server_new_server_field"], future_server_field, "future_server_field stille exists");
782 CKKSItem* newItem = [[CKKSItem alloc] initWithCKRecord:newRecord];
783 CKKSAESSIVKey* newItemKey = [self.keychainZoneKeys.classA unwrapAESKey:newItem.wrappedkey error:&error];
784 XCTAssertNil(error, "No error unwrapping AES key");
785 XCTAssertNotNil(newItemKey, "Have an unwrapped AES key for this item");
787 NSDictionary* uploadedData = [CKKSItemEncrypter decryptDictionary:newRecord[SecCKRecordDataKey]
789 authenticatedData:authenticatedData
791 XCTAssertNil(error, "No error decrypting dictionary");
792 XCTAssertNotNil(uploadedData, "Authenticated re-uploaded data including future_field");
793 XCTAssertEqualObjects(uploadedData[@"v_Data"], [@"different password" dataUsingEncoding:NSUTF8StringEncoding], "Passwords match");
797 -(void)testReceiveRecordEncryptedv1 {
798 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
800 [self startCKKSSubsystem];
801 [self.keychainView waitForKeyHierarchyReadiness];
803 NSError* error = nil;
805 // Manually encrypt an item
806 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
807 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
808 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
809 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
810 parentKeyUUID:self.keychainZoneKeys.classC.uuid
811 zoneID:recordID.zoneID];
812 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
813 XCTAssertNotNil(itemkey, "Got a key");
814 cipheritem.wrappedkey = itemkey.wrappedkey;
815 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
817 cipheritem.encver = CKKSItemEncryptionVersion1;
819 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:cipheritem.encver] mutableCopy];
821 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
822 XCTAssertNil(error, "no error encrypting object");
823 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
825 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
827 [self.keychainView waitForFetchAndIncomingQueueProcessing];
829 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
830 (id)kSecReturnAttributes: @YES,
831 (id)kSecAttrSynchronizable: @YES,
832 (id)kSecAttrAccount: @"account-delete-me",
833 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
835 CFTypeRef cfresult = NULL;
836 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
837 CFReleaseNull(cfresult);
839 // Test that if this item is updated, it is encrypted in v2
840 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
841 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
843 OCMVerifyAllWithDelay(self.mockDatabase, 4);
844 [self waitForCKModifications];
846 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
847 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
850 - (void)testUploadPagination {
851 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
853 for(size_t count = 0; count < 250; count++) {
854 [self addGenericPassword: @"data" account: [NSString stringWithFormat:@"account-delete-me-%03lu", count]];
857 [self startCKKSSubsystem];
859 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
860 [self expectCKModifyItemRecords: SecCKKSOutgoingQueueItemsAtOnce currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
861 [self expectCKModifyItemRecords: 50 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
863 OCMVerifyAllWithDelay(self.mockDatabase, 160);
866 - (void)testUploadInitialKeyHierarchy {
867 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
868 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
870 // Spin up CKKS subsystem.
871 [self startCKKSSubsystem];
873 OCMVerifyAllWithDelay(self.mockDatabase, 8);
876 - (void)testUploadInitialKeyHierarchyAfterLockedStart {
878 self.aksLockState = true;
879 [self.lockStateTracker recheck];
881 [self startCKKSSubsystem];
883 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
884 while(!([self.keychainView.keyStateMachineOperation isPending] && [self.keychainView.keyStateMachineOperation.dependencies containsObject:self.lockStateTracker.unlockDependency])) {
888 // After unlock, the key hierarchy should be created.
889 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
891 self.aksLockState = false;
892 [self.lockStateTracker recheck];
894 OCMVerifyAllWithDelay(self.mockDatabase, 8);
896 // We expect a single class C record to be uploaded.
897 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
899 [self addGenericPassword: @"data" account: @"account-delete-me"];
900 OCMVerifyAllWithDelay(self.mockDatabase, 8);
903 - (void)testReceiveKeyHierarchyAfterLockedStart {
905 self.aksLockState = true;
906 [self.lockStateTracker recheck];
908 [self startCKKSSubsystem];
910 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
911 while(!([self.keychainView.keyStateMachineOperation isPending] && [self.keychainView.keyStateMachineOperation.dependencies containsObject:self.lockStateTracker.unlockDependency])) {
915 // Now, another device comes along and creates the hierarchy; we download it; and it and sends us the TLK
916 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
917 [self.keychainView notifyZoneChange:nil];
918 [[self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting] waitUntilFinished];
920 self.aksLockState = false;
921 [self.lockStateTracker recheck];
923 // After unlock, the TLK arrives
924 [self saveTLKMaterialToKeychain:self.keychainZoneID];
926 // We expect a single class C record to be uploaded.
927 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
929 [self addGenericPassword: @"data" account: @"account-delete-me"];
930 OCMVerifyAllWithDelay(self.mockDatabase, 8);
933 - (void)testUploadAndUseKeyHierarchy {
934 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
935 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
937 [self startCKKSSubsystem];
939 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
940 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
941 (id)kSecAttrAccount : @"account-delete-me",
942 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
943 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
945 CFTypeRef item = NULL;
946 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not exist");
948 OCMVerifyAllWithDelay(self.mockDatabase, 1);
950 [self waitForCKModifications];
952 // We expect a single class C record to be uploaded.
953 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
955 [self addGenericPassword: @"data" account: @"account-delete-me"];
956 OCMVerifyAllWithDelay(self.mockDatabase, 8);
958 // now, expect a single class A record to be uploaded
959 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
961 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef)@{
962 (id)kSecClass : (id)kSecClassGenericPassword,
963 (id)kSecAttrAccessGroup : @"com.apple.security.sos",
964 (id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked,
965 (id)kSecAttrAccount : @"account-class-A",
966 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
967 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
968 }, NULL), @"Adding class A item");
969 OCMVerifyAllWithDelay(self.mockDatabase, 8);
972 - (void)testUploadInitialKeyHierarchyTriggersBackup {
973 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
974 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
976 // We also expect the view manager's notifyNewTLKsInKeychain call to fire (after some delay)
977 id mockVM = OCMPartialMock(self.injectedManager);
978 OCMExpect([mockVM notifyNewTLKsInKeychain]);
980 // Spin up CKKS subsystem.
981 [self startCKKSSubsystem];
983 OCMVerifyAllWithDelay(self.mockDatabase, 8);
984 OCMVerifyAllWithDelay(mockVM, 10);
986 [mockVM stopMocking];
989 - (void)testAcceptExistingKeyHierarchy {
990 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
991 // Test also begins with the TLK having arrived in the local keychain (via SOS)
992 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
993 [self saveTLKMaterialToKeychain:self.keychainZoneID];
995 // Spin up CKKS subsystem.
996 [self startCKKSSubsystem];
998 // The CKKS subsystem should not try to write anything to the CloudKit database while it's accepting the keys
999 [self.keychainView waitForKeyHierarchyReadiness];
1001 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1003 // Verify that there are three local keys, and three local current key records
1004 __weak __typeof(self) weakSelf = self;
1005 [self.keychainView dispatchSync: ^bool{
1006 __strong __typeof(weakSelf) strongSelf = weakSelf;
1007 XCTAssertNotNil(strongSelf, "self exists");
1009 NSError* error = nil;
1011 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
1012 XCTAssertNil(error, "no error fetching keys");
1013 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
1015 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1016 XCTAssertNil(error, "no error fetching current keys");
1017 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1023 - (void)testAcceptExistingAndUseKeyHierarchy {
1024 // Test starts with nothing in database, but one in our fake CloudKit.
1025 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1027 // Spin up CKKS subsystem.
1028 [self startCKKSSubsystem];
1030 // The CKKS subsystem should not try to write anything to the CloudKit database.
1033 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1035 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
1036 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1038 // Wait for the key hierarchy to sort itself out, to make it easier on this test; see testOnboardOldItemsWithExistingKeyHierarchy for the other test.
1039 [self.keychainView waitForKeyHierarchyReadiness];
1041 // We expect a single record to be uploaded for each key class
1042 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1043 [self addGenericPassword: @"data" account: @"account-delete-me"];
1044 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1046 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1047 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef)@{
1048 (id)kSecClass : (id)kSecClassGenericPassword,
1049 (id)kSecAttrAccessGroup : @"com.apple.security.sos",
1050 (id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked,
1051 (id)kSecAttrAccount : @"account-class-A",
1052 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1053 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
1054 }, NULL), @"Adding class A item");
1055 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1060 - (void)testAcceptExistingKeyHierarchyDespiteLocked {
1061 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
1062 // Test also begins with the TLK having arrived in the local keychain (via SOS)
1063 // However, the CKKSKeychainView's "checkTLK" method should return a keychain error the first time through, indicating that the keybag is locked
1064 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1065 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1067 self.aksLockState = true;
1068 [self.lockStateTracker recheck];
1070 id partialKVMock = OCMPartialMock(self.keychainView);
1071 OCMExpect([partialKVMock checkTLK: [OCMArg any] error: [OCMArg setTo:[[NSError alloc] initWithDomain:@"securityd" code:errSecInteractionNotAllowed userInfo:nil]]]).andReturn(false);
1073 // Spin up CKKS subsystem.
1074 [self startCKKSSubsystem];
1076 OCMVerifyAllWithDelay(partialKVMock, 4);
1078 // Now that all operations are complete, 'unlock' AKS
1079 self.aksLockState = false;
1080 [self.lockStateTracker recheck];
1082 [self.keychainView waitForKeyHierarchyReadiness];
1083 OCMVerifyAllWithDelay(self.mockDatabase, 4);
1085 // Verify that there are three local keys, and three local current key records
1086 __weak __typeof(self) weakSelf = self;
1087 [self.keychainView dispatchSync: ^bool{
1088 __strong __typeof(weakSelf) strongSelf = weakSelf;
1089 XCTAssertNotNil(strongSelf, "self exists");
1091 NSError* error = nil;
1093 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
1094 XCTAssertNil(error, "no error fetching keys");
1095 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
1097 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1098 XCTAssertNil(error, "no error fetching current keys");
1099 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1104 [partialKVMock stopMocking];
1107 - (void)testReceiveClassCWhileALocked {
1108 // Test starts with a key hierarchy already existing.
1109 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
1110 [self startCKKSSubsystem];
1112 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1113 [self.keychainView waitForKeyHierarchyReadiness];
1115 [self findGenericPassword:@"classCItem" expecting:errSecItemNotFound];
1116 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
1118 // 'Lock' the keybag
1119 self.aksLockState = true;
1120 [self.lockStateTracker recheck];
1122 XCTAssertNotNil(self.keychainZoneKeys, "Have zone keys for zone");
1123 XCTAssertNotNil(self.keychainZoneKeys.classA, "Have class A key for zone");
1124 XCTAssertNotNil(self.keychainZoneKeys.classC, "Have class C key for zone");
1126 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"classCItem" key:self.keychainZoneKeys.classC]];
1127 [self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-FFFF-FFFF-FFFF-5A507ACB2D85" withAccount:@"classAItem" key:self.keychainZoneKeys.classA]];
1129 CKKSResultOperation* op = [self.keychainView waitForFetchAndIncomingQueueProcessing];
1130 // The processing op should NOT error, even though it didn't manage to process the classA item
1131 XCTAssertNil(op.error, "no error while failing to process a class A item");
1133 CKKSResultOperation* erroringOp = [self.keychainView processIncomingQueue:true];
1134 [erroringOp waitUntilFinished];
1135 XCTAssertNotNil(erroringOp.error, "error exists while processing a class A item");
1137 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
1138 [self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
1140 self.aksLockState = false;
1141 [self.lockStateTracker recheck];
1142 [self.keychainView waitUntilAllOperationsAreFinished];
1144 [self findGenericPassword:@"classCItem" expecting:errSecSuccess];
1145 [self findGenericPassword:@"classAItem" expecting:errSecSuccess];
1148 - (void)testExternalKeyRoll {
1149 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1150 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1151 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1153 // Spin up CKKS subsystem.
1154 [self startCKKSSubsystem];
1156 // The CKKS subsystem should not try to write anything to the CloudKit database.
1157 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1159 __weak __typeof(self) weakSelf = self;
1161 // We expect a single record to be uploaded.
1162 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1164 [self addGenericPassword: @"data" account: @"account-delete-me"];
1166 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1167 [self waitForCKModifications];
1169 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1170 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1172 // Trigger a notification
1173 [self.keychainView notifyZoneChange:nil];
1175 // Make life easy on this test; testAcceptKeyConflictAndUploadReencryptedItem will check the case when we don't receive the notification
1176 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1178 // Verify that there are six local keys, and three local current key records
1179 [self.keychainView dispatchSync: ^bool{
1180 __strong __typeof(weakSelf) strongSelf = weakSelf;
1181 XCTAssertNotNil(strongSelf, "self exists");
1183 NSError* error = nil;
1184 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:self.keychainZoneID error:&error];
1185 XCTAssertNil(error, "no error fetching keys");
1186 XCTAssertEqual(keys.count, 6u, "Six keys in local database");
1188 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all: &error];
1189 XCTAssertNil(error, "no error fetching current keys");
1190 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
1192 for(CKKSCurrentKeyPointer* key in currentkeys) {
1193 if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
1194 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.tlk.uuid);
1195 } else if([key.keyclass isEqualToString: SecCKKSKeyClassA]) {
1196 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classA.uuid);
1197 } else if([key.keyclass isEqualToString: SecCKKSKeyClassC]) {
1198 XCTAssertEqualObjects(key.currentKeyUUID, strongSelf.keychainZoneKeys.classC.uuid);
1200 XCTFail("Unknown key class: %@", key.keyclass);
1207 // We expect a single record to be uploaded.
1208 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1210 // TODO: remove this by writing code for item reencrypt after key arrival
1211 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1213 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1215 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1218 - (void)testAcceptKeyConflictAndUploadReencryptedItem {
1219 // Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
1220 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1221 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1223 [self startCKKSSubsystem];
1224 [self.keychainView waitUntilAllOperationsAreFinished];
1226 // We expect a single record to be uploaded.
1227 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1229 [self addGenericPassword: @"data" account: @"account-delete-me"];
1231 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1232 [self waitForCKModifications];
1234 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1236 // Do not trigger a notification here. This should cause a conflict updating the current key records
1238 // We expect a single record to be uploaded, but that the write will be rejected
1239 // We then expect that item to be reuploaded with the current key
1241 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
1242 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
1243 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1245 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under rolled class C key in hierarchy"]];
1247 // New key arrives via SOS!
1248 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1250 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1253 - (void)testOnboardOldItems {
1254 // 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
1256 // Test starts with nothing in CloudKit, and CKKS blocked. Add one item without a UUID...
1258 SecCKKSTestSetDisableAutomaticUUID(true);
1259 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1261 // and an item with a UUID...
1262 SecCKKSTestSetDisableAutomaticUUID(false);
1263 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1265 // We expect an upload of the key hierarchy
1266 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
1268 // We then expect an upload of the added items
1269 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1271 [self startCKKSSubsystem];
1273 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1277 - (void)testOnboardOldItemsWithExistingKeyHierarchyExtantTLK {
1278 // Test starts key hierarchy in our fake CloudKit, the TLK arrived in the local keychain, and CKKS blocked.
1279 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1280 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1282 // Add one item without a UUID...
1283 SecCKKSTestSetDisableAutomaticUUID(true);
1284 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1286 // and an item with a UUID...
1287 SecCKKSTestSetDisableAutomaticUUID(false);
1288 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1290 // 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
1291 // We expect a single record to be uploaded.
1292 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1294 // Spin up CKKS subsystem.
1295 [self startCKKSSubsystem];
1297 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1300 - (void)testOnboardOldItemsWithExistingKeyHierarchyLateTLK {
1301 // Test starts key hierarchy in our fake CloudKit, and CKKS blocked.
1302 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1304 // Add one item without a UUID...
1305 SecCKKSTestSetDisableAutomaticUUID(true);
1306 [self addGenericPassword: @"data" account: @"account-delete-me-no-UUID" expecting:errSecSuccess message: @"Add item (no UUID) to keychain"];
1308 // and an item with a UUID...
1309 SecCKKSTestSetDisableAutomaticUUID(false);
1310 [self addGenericPassword: @"data" account: @"account-delete-me-with-UUID" expecting:errSecSuccess message: @"Add item (w/ UUID) to keychain"];
1312 // 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
1314 // Spin up CKKS subsystem.
1315 [self startCKKSSubsystem];
1319 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1321 // Now, save the TLK to the keychain (to simulate it coming in via SOS).
1322 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1324 // We expect a single record to be uploaded.
1325 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1327 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1330 - (void)testResync {
1331 // We need to set up a desynced situation to test our resync.
1332 // First, let CKKS start up and send several items to CloudKit (that we'll then desync!)
1333 __block NSError* error = nil;
1335 // Test starts with keys in CloudKit (so we can create items later)
1336 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1337 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1339 [self addGenericPassword: @"data" account: @"first"];
1340 [self addGenericPassword: @"data" account: @"second"];
1341 [self addGenericPassword: @"data" account: @"third"];
1342 [self addGenericPassword: @"data" account: @"fourth"];
1343 NSUInteger passwordCount = 4u;
1345 [self checkGenericPassword: @"data" account: @"first"];
1346 [self checkGenericPassword: @"data" account: @"second"];
1347 [self checkGenericPassword: @"data" account: @"third"];
1348 [self checkGenericPassword: @"data" account: @"fourth"];
1350 [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1352 [self startCKKSSubsystem];
1354 // Wait for uploads to happen
1355 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1356 [self waitForCKModifications];
1357 XCTAssertEqual(self.keychainZone.currentDatabase.count, SYSTEM_DB_RECORD_COUNT+passwordCount, "Have 6+passwordCount objects in cloudkit");
1359 // Now, corrupt away!
1360 // Extract all passwordCount items for Corruption
1361 NSArray<CKRecord*>* items = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
1362 XCTAssertEqual(items.count, passwordCount, "Have %lu Items in cloudkit", (unsigned long)passwordCount);
1364 // For the first record, delete all traces of it from CKKS. But! it remains in local keychain.
1365 // Expected outcome: CKKS resyncs; item exists again.
1366 CKRecord* delete = items[0];
1367 NSString* deleteAccount = [[self decryptRecord: delete] objectForKey: (__bridge id) kSecAttrAccount];
1368 XCTAssertNotNil(deleteAccount, "received an account for the local delete object");
1370 __weak __typeof(self) weakSelf = self;
1371 [self.keychainView dispatchSync:^bool{
1372 __strong __typeof(weakSelf) strongSelf = weakSelf;
1373 XCTAssertNotNil(strongSelf, "self exists");
1375 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1377 [ckme deleteFromDatabase: &error];
1379 XCTAssertNil(error, "no error removing CKME");
1380 CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1382 [oqe deleteFromDatabase: &error];
1384 XCTAssertNil(error, "no error removing OQE");
1385 CKKSIncomingQueueEntry* iqe = [CKKSIncomingQueueEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1387 [iqe deleteFromDatabase: &error];
1389 XCTAssertNil(error, "no error removing IQE");
1393 // For the second record, delete all traces of it from CloudKit.
1394 // Expected outcome: deleted locally
1395 CKRecord* remoteDelete = items[1];
1396 NSString* remoteDeleteAccount = [[self decryptRecord: remoteDelete] objectForKey: (__bridge id) kSecAttrAccount];
1397 XCTAssertNotNil(remoteDeleteAccount, "received an account for the remote delete object");
1399 [self.keychainZone deleteCKRecordIDFromZone: remoteDelete.recordID];
1400 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
1401 [database removeObjectForKey: remoteDelete.recordID];
1404 // The third record gets modified in CloudKit, but not locally.
1405 // Expected outcome: use the CloudKit version
1406 CKRecord* remoteDataChanged = items[2];
1407 NSMutableDictionary* remoteDataDictionary = [[self decryptRecord: remoteDataChanged] mutableCopy];
1408 NSString* remoteDataChangedAccount = [remoteDataDictionary objectForKey: (__bridge id) kSecAttrAccount];
1409 XCTAssertNotNil(remoteDataChangedAccount, "Received an account for the remote-data-changed object");
1410 remoteDataDictionary[(__bridge id) kSecValueData] = [@"CloudKitWins" dataUsingEncoding: NSUTF8StringEncoding];
1412 CKRecord* newData = [self newRecord: remoteDataChanged.recordID withNewItemData: remoteDataDictionary];
1413 [self.keychainZone addToZone: newData];
1414 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
1415 database[remoteDataChanged.recordID] = newData;
1418 // The fourth record stays in-sync. Good work, everyone!
1419 // Expected outcome: stays in-sync
1420 NSString* insyncAccount = [[self decryptRecord: items[3]] objectForKey: (__bridge id) kSecAttrAccount];
1421 XCTAssertNotNil(insyncAccount, "Received an account for the in-sync object");
1423 // The fifth record gets magically added to CloudKit, but CKKS has never heard of it
1424 // (emulates a lost record on the client, but that CloudKit already believes it's sent the record for)
1425 // Expected outcome: added to local keychain
1426 NSString* remoteOnlyAccount = @"remote-only";
1427 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount: remoteOnlyAccount];
1428 [self.keychainZone addToZone: ckr];
1429 for(NSMutableDictionary<CKRecordID*, CKRecord*>* database in self.keychainZone.pastDatabases.allValues) {
1430 database[ckr.recordID] = ckr;
1433 ckksnotice("ckksresync", self.keychainView, "local delete: %@ %@", delete.recordID.recordName, deleteAccount);
1434 ckksnotice("ckksresync", self.keychainView, "Remote deletion: %@ %@", remoteDelete.recordID.recordName, remoteDeleteAccount);
1435 ckksnotice("ckksresync", self.keychainView, "Remote data changed: %@ %@", remoteDataChanged.recordID.recordName, remoteDataChangedAccount);
1436 ckksnotice("ckksresync", self.keychainView, "in-sync: %@ %@", items[3].recordID.recordName, insyncAccount);
1437 ckksnotice("ckksresync", self.keychainView, "Remote only: %@ %@", ckr.recordID.recordName, remoteOnlyAccount);
1439 CKKSSynchronizeOperation* resyncOperation = [self.keychainView resyncWithCloud];
1440 [resyncOperation waitUntilFinished];
1442 XCTAssertNil(resyncOperation.error, "No error during the resync operation");
1444 // Now do some checking. Remember, we don't know which record we corrupted, so use the parsed account variables to check.
1446 [self findGenericPassword: deleteAccount expecting: errSecSuccess];
1447 [self findGenericPassword: remoteDeleteAccount expecting: errSecItemNotFound];
1448 [self findGenericPassword: remoteDataChangedAccount expecting: errSecSuccess];
1449 [self findGenericPassword: insyncAccount expecting: errSecSuccess];
1450 [self findGenericPassword: remoteOnlyAccount expecting: errSecSuccess];
1452 [self checkGenericPassword: @"data" account: deleteAccount];
1453 //[self checkGenericPassword: @"data" account: remoteDeleteAccount];
1454 [self checkGenericPassword: @"CloudKitWins" account: remoteDataChangedAccount];
1455 [self checkGenericPassword: @"data" account: insyncAccount];
1456 [self checkGenericPassword: @"data" account: remoteOnlyAccount];
1458 [self.keychainView dispatchSync:^bool{
1459 __strong __typeof(weakSelf) strongSelf = weakSelf;
1460 XCTAssertNotNil(strongSelf, "self exists");
1462 CKKSMirrorEntry* ckme = nil;
1464 ckme = [CKKSMirrorEntry tryFromDatabase:delete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1465 XCTAssertNil(error);
1466 XCTAssertNotNil(ckme);
1468 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDelete.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1469 XCTAssertNil(error);
1470 XCTAssertNil(ckme); // deleted!
1472 ckme = [CKKSMirrorEntry tryFromDatabase:remoteDataChanged.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1473 XCTAssertNil(error);
1474 XCTAssertNotNil(ckme);
1476 ckme = [CKKSMirrorEntry tryFromDatabase:items[3].recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1477 XCTAssertNil(error);
1478 XCTAssertNotNil(ckme);
1480 ckme = [CKKSMirrorEntry tryFromDatabase:ckr.recordID.recordName zoneID:strongSelf.keychainZoneID error:&error];
1481 XCTAssertNil(error);
1482 XCTAssertNotNil(ckme);
1487 - (void)testMultipleZoneAdd {
1488 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
1489 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
1491 // Bring up a new zone: we expect a key hierarchy upload.
1492 [self.injectedManager findOrCreateView:(id)kSecAttrViewHintAppleTV];
1493 CKRecordZoneID* appleTVZoneID = [[CKRecordZoneID alloc] initWithZoneName:(__bridge NSString*) kSecAttrViewHintAppleTV ownerName:CKCurrentUserDefaultName];
1494 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:appleTVZoneID];
1496 // We also expect the view manager's notifyNewTLKsInKeychain call to fire once (after some delay)
1497 id mockVM = OCMPartialMock(self.injectedManager);
1498 OCMExpect([mockVM notifyNewTLKsInKeychain]);
1500 // Let the horses loose
1501 [self startCKKSSubsystem];
1503 // We expect a single record to be uploaded to the 'keychain' view
1504 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1505 [self addGenericPassword: @"data" account: @"account-delete-me"];
1506 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1508 // We expect a single record to be uploaded to the 'atv' view
1509 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:appleTVZoneID];
1510 [self addGenericPassword: @"atv"
1511 account: @"tvaccount"
1512 viewHint:(__bridge NSString*) kSecAttrViewHintAppleTV
1513 access:(id)kSecAttrAccessibleAfterFirstUnlock
1514 expecting:errSecSuccess message:@"AppleTV view-hinted object"];
1516 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1518 OCMVerifyAllWithDelay(mockVM, 10);
1519 [mockVM stopMocking];
1522 - (void)testMultipleZoneDelete {
1523 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
1524 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
1526 [self startCKKSSubsystem];
1528 // We expect a single record to be uploaded.
1529 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
1530 [self addGenericPassword: @"data" account: @"account-delete-me"];
1531 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1533 // Bring up a new zone: we expect a key hierarchy and an item.
1534 [self.injectedManager findOrCreateView:(id)kSecAttrViewHintAppleTV];
1535 CKRecordZoneID* appleTVZoneID = [[CKRecordZoneID alloc] initWithZoneName:(__bridge NSString*) kSecAttrViewHintAppleTV ownerName:CKCurrentUserDefaultName];
1536 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:appleTVZoneID];
1537 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:appleTVZoneID];
1539 [self addGenericPassword: @"atv"
1540 account: @"tvaccount"
1541 viewHint:(__bridge NSString*) kSecAttrViewHintAppleTV
1542 access:(id)kSecAttrAccessibleAfterFirstUnlock
1543 expecting:errSecSuccess
1544 message:@"AppleTV view-hinted object"];
1545 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1547 // We expect a single record to be deleted from the ATV zone
1548 [self expectCKDeleteItemRecords: 1 zoneID:appleTVZoneID];
1549 [self deleteGenericPassword:@"tvaccount"];
1550 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1552 // Now we expect a single record to be deleted from the test zone
1553 [self expectCKDeleteItemRecords: 1 zoneID:self.keychainZoneID];
1554 [self deleteGenericPassword:@"account-delete-me"];
1555 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1558 - (void)testRestartWithoutRefetch {
1559 // Restarting the CKKS operation should check that it's been 15 minutes since the last fetch before it fetches again. Simulate this.
1561 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
1562 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
1563 [self startCKKSSubsystem];
1565 [self.keychainView waitForKeyHierarchyReadiness];
1566 [self waitForCKModifications];
1567 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1569 // Tear down the CKKS object and disallos fetches
1570 [self.keychainView cancelAllOperations];
1571 self.silentFetchesAllowed = false;
1573 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1574 [self.keychainView waitForKeyHierarchyReadiness];
1575 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1577 // Okay, cool, rad, now let's set the date to be very long ago and check that there's positively a fetch
1578 [self.keychainView cancelAllOperations];
1579 self.silentFetchesAllowed = false;
1581 [self.keychainView dispatchSync: ^bool {
1582 NSError* error = nil;
1583 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
1585 XCTAssertNil(error, "no error pulling ckse from database");
1586 XCTAssertNotNil(ckse, "received a ckse");
1588 ckse.lastFetchTime = [NSDate distantPast];
1589 [ckse saveToDatabase: &error];
1590 XCTAssertNil(error, "no error saving to database");
1594 [self expectCKFetch];
1595 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1596 [self.keychainView waitForKeyHierarchyReadiness];
1597 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1600 - (void)testRecoverFromZoneCreationFailure {
1601 // Fail the zone creation.
1602 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
1603 [self failNextZoneCreation:self.keychainZoneID];
1605 // Spin up CKKS subsystem.
1606 [self startCKKSSubsystem];
1608 // The CKKS subsystem should figure out the issue, and fix it.
1609 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
1611 [self.keychainView waitForKeyHierarchyReadiness];
1612 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1614 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1615 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1616 [self addGenericPassword: @"data" account: @"account-delete-me"];
1617 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1619 XCTAssertNil(self.zones[self.keychainZoneID].creationError, "Creation error was unset (and so CKKS probably dealt with the error");
1622 - (void)testRecoverFromZoneSubscriptionFailure {
1623 // Fail the zone subscription.
1624 [self failNextZoneSubscription:self.keychainZoneID];
1626 // Spin up CKKS subsystem.
1627 [self startCKKSSubsystem];
1629 // The CKKS subsystem should figure out the issue, and fix it.
1630 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
1632 [self.keychainView waitForKeyHierarchyReadiness];
1633 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1635 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1636 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1637 [self addGenericPassword: @"data" account: @"account-delete-me"];
1638 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1640 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
1643 - (void)testRecoverFromZoneSubscriptionFailureDueToZoneNotExisting {
1644 // This is different from testRecoverFromZoneSubscriptionFailure, since the zone is gone. CKKS must attempt to re-create the zone.
1646 // Silently fail the zone creation
1647 self.zones[self.keychainZoneID] = nil; // delete the autocreated zone
1648 [self failNextZoneCreationSilently:self.keychainZoneID];
1650 // Spin up CKKS subsystem.
1651 [self startCKKSSubsystem];
1653 // The CKKS subsystem should figure out the issue, and fix it.
1654 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
1656 [self.keychainView waitForKeyHierarchyReadiness];
1657 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1659 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1660 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1661 [self addGenericPassword: @"data" account: @"account-delete-me"];
1662 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1664 XCTAssertFalse(self.zones[self.keychainZoneID].flag, "Zone flag was reset");
1665 XCTAssertNil(self.zones[self.keychainZoneID].subscriptionError, "Subscription error was unset (and so CKKS probably dealt with the error");
1668 - (void)testRecoverFromDeletedTLKWithStashedTLK {
1669 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
1671 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1672 NSError* error = nil;
1673 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
1674 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
1676 // Spin up CKKS subsystem.
1677 [self startCKKSSubsystem];
1679 [self.keychainView waitForKeyHierarchyReadiness];
1680 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1682 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1683 [self addGenericPassword: @"data" account: @"account-delete-me"];
1684 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1686 // CKKS should recreate the syncable TLK.
1687 [self checkNSyncableTLKsInKeychain: 1];
1690 - (void)testRecoverFromDeletedTLKWithStashedTLKUponRestart {
1691 // We need to handle the case where our syncable TLKs are deleted for some reason. The device that has them might resurrect them
1693 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1694 // Spin up CKKS subsystem.
1695 [self startCKKSSubsystem];
1696 [self.keychainView waitForKeyHierarchyReadiness];
1698 // Tear down the CKKS object
1699 [self.keychainView cancelAllOperations];
1701 NSError* error = nil;
1702 [self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
1703 XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
1705 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1706 [self.keychainView waitForKeyHierarchyReadiness];
1707 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1709 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1710 [self addGenericPassword: @"data" account: @"account-delete-me"];
1711 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1713 // CKKS should recreate the syncable TLK.
1714 [self checkNSyncableTLKsInKeychain: 1];
1717 - (void)testRecoverFromTLKWriteFailure {
1718 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
1719 // Test starts with nothing in CloudKit, and will fail the first TLK write.
1720 NSError* noNetwork = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}];
1721 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:noNetwork];
1723 // Spin up CKKS subsystem.
1724 [self startCKKSSubsystem];
1726 // The CKKS subsystem should figure out the issue, and fix it.
1727 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
1729 [self.keychainView waitForKeyHierarchyReadiness];
1730 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1732 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1733 [self addGenericPassword: @"data" account: @"account-delete-me"];
1734 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1736 // A network failure creating new TLKs shouldn't delete the 'failed' syncable one.
1737 [self checkNSyncableTLKsInKeychain: 2];
1740 - (void)testRecoverFromTLKRace {
1741 // We need to handle the case where a device's first TLK write doesn't go through (due to whatever reason).
1742 // Test starts with nothing in CloudKit, and will fail the first TLK write.
1743 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject: ^{
1744 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1747 // Spin up CKKS subsystem.
1748 [self startCKKSSubsystem];
1750 // The first TLK write should fail, and then our fake TLKs should be there in CloudKit.
1751 // It shouldn't write anything back up to CloudKit.
1752 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1754 // Now the TLKs arrive from the other device...
1755 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1756 [self.keychainView waitForKeyHierarchyReadiness];
1758 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1759 [self addGenericPassword: @"data" account: @"account-delete-me"];
1760 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1762 // A race failure creating new TLKs should delete the old syncable one.
1763 [self checkNSyncableTLKsInKeychain: 1];
1766 - (void)testRecoverFromNullCurrentKeyPointers {
1767 // The current key pointers in cloudkit shouldn't ever not exist if keys do. But, if they don't, CKKS must recover.
1769 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
1770 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1771 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1773 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
1774 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
1775 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
1776 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
1777 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = nil;
1779 // Spin up CKKS subsystem.
1780 [self startCKKSSubsystem];
1782 // The CKKS subsystem should figure out the issue, and fix it.
1783 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
1785 [self.keychainView waitForKeyHierarchyReadiness];
1787 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1790 - (void)testRecoverFromNoCurrentKeyPointers {
1791 // The current key pointers in cloudkit shouldn't ever point to nil. But, if they do, CKKS must recover.
1793 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
1794 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1795 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1797 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
1798 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentTLKPointer.storedCKRecord.recordID], "Deleted TLK pointer from zone");
1799 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassAPointer.storedCKRecord.recordID], "Deleted class a pointer from zone");
1800 XCTAssertNil([self.zones[self.keychainZoneID] deleteCKRecordIDFromZone: zonekeys.currentClassCPointer.storedCKRecord.recordID], "Deleted class c pointer from zone");
1802 // Spin up CKKS subsystem.
1803 [self startCKKSSubsystem];
1805 // The CKKS subsystem should figure out the issue, and fix it.
1806 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
1808 [self.keychainView waitForKeyHierarchyReadiness];
1810 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1813 - (void)testRecoverFromBadChangeTag {
1814 // 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.
1816 // Test starts with a broken key hierarchy in our fake CloudKit, but a (incorrectly) up-to-date change tag stored locally.
1817 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1818 SecCKKSTestSetDisableKeyNotifications(true); // Don't tell CKKS about this key material; we're pretending like this is a securityd restart
1819 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1820 SecCKKSTestSetDisableKeyNotifications(false);
1822 [self.keychainView dispatchSync: ^bool {
1823 NSError* error = nil;
1824 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainZoneID.zoneName];
1825 XCTAssertNotNil(ckse, "should have received a ckse");
1827 ckse.ckzonecreated = true;
1828 ckse.ckzonesubscribed = true;
1829 ckse.changeToken = self.keychainZone.currentChangeToken;
1831 [ckse saveToDatabase: &error];
1832 XCTAssertNil(error, "shouldn't have gotten an error saving to database");
1836 // Spin up CKKS subsystem.
1837 [self startCKKSSubsystem];
1839 // The CKKS subsystem should try to write TLKs, but fail
1840 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
1841 OCMVerifyAllWithDelay(self.mockDatabase, 16);
1843 // CKKS should then happily use the keys in CloudKit
1844 [self createClassCItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me"];
1845 [self createClassAItemAndWaitForUpload:self.keychainZoneID account:@"account-delete-me-class-a"];
1848 - (void)testRecoverFromDeletedKeysNewItem {
1849 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
1850 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
1852 [self startCKKSSubsystem];
1853 [self.keychainView waitForKeyHierarchyReadiness];
1855 // We expect a single class C record to be uploaded.
1856 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1857 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1859 [self addGenericPassword: @"data" account: @"account-delete-me"];
1860 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1862 [self waitForCKModifications];
1863 [self.keychainView waitUntilAllOperationsAreFinished];
1865 // Now, delete the local keys from the keychain (but leave the synced TLK)
1866 SecCKKSTestSetDisableKeyNotifications(true);
1867 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
1868 (id)kSecClass : (id)kSecClassInternetPassword,
1869 (id)kSecAttrNoLegacy : @YES,
1870 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1871 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
1872 }), @"Deleting local keys");
1873 SecCKKSTestSetDisableKeyNotifications(false);
1875 NSError* error = nil;
1876 [self.keychainZoneKeys.classC loadKeyMaterialFromKeychain:&error];
1877 XCTAssertNotNil(error, "Error loading class C key material from keychain");
1879 // We expect a single class C record to be uploaded.
1880 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1881 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1883 [self addGenericPassword: @"datadata" account: @"account-no-keys"];
1884 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1886 // We expect a single class A record to be uploaded.
1887 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1888 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1889 [self addGenericPassword:@"asdf"
1890 account:@"account-class-A"
1892 access:(id)kSecAttrAccessibleWhenUnlocked
1893 expecting:errSecSuccess
1894 message:@"Adding class A item"];
1895 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1898 - (void)testRecoverFromDeletedKeysReceive {
1899 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
1900 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
1902 [self startCKKSSubsystem];
1903 [self.keychainView waitForKeyHierarchyReadiness];
1905 [self waitForCKModifications];
1906 [self.keychainView waitUntilAllOperationsAreFinished];
1908 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1910 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
1912 // Now, delete the local keys from the keychain (but leave the synced TLK)
1913 SecCKKSTestSetDisableKeyNotifications(true);
1914 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
1915 (id)kSecClass : (id)kSecClassInternetPassword,
1916 (id)kSecAttrNoLegacy : @YES,
1917 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1918 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
1919 }), @"Deleting local keys");
1920 SecCKKSTestSetDisableKeyNotifications(false);
1922 // Trigger a notification (with hilariously fake data)
1923 [self.keychainZone addToZone: ckr];
1924 [self.keychainView notifyZoneChange:nil];
1925 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1926 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1928 [self findGenericPassword: @"account0" expecting:errSecSuccess];
1931 - (void)disabledtestRecoverDeletedTLKAndPause {
1932 // If the TLK disappears halfway through, well, that's no good. But we should make it into waitfortlk.
1934 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
1935 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
1937 [self startCKKSSubsystem];
1938 [self.keychainView waitForKeyHierarchyReadiness];
1940 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1942 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
1943 [self.keychainView waitUntilAllOperationsAreFinished];
1945 // Now, delete the local keys from the keychain
1946 SecCKKSTestSetDisableKeyNotifications(true);
1947 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
1948 (id)kSecClass : (id)kSecClassInternetPassword,
1949 (id)kSecAttrNoLegacy : @YES,
1950 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1951 (id)kSecAttrSynchronizable : (id)kCFBooleanFalse,
1952 }), @"Deleting local keys");
1953 XCTAssertEqual(errSecSuccess, SecItemDelete((__bridge CFDictionaryRef)@{
1954 (id)kSecClass : (id)kSecClassInternetPassword,
1955 (id)kSecAttrNoLegacy : @YES,
1956 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
1957 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
1958 }), @"Deleting TLK");
1959 SecCKKSTestSetDisableKeyNotifications(false);
1961 // Trigger a notification (with hilariously fake data)
1962 [self.keychainZone addToZone: ckr];
1963 [self.keychainView notifyZoneChange:nil];
1965 [self.keychainView waitForFetchAndIncomingQueueProcessing];
1966 [self.keychainView waitForOperationsOfClass:[CKKSHealKeyHierarchyOperation class]];
1968 XCTAssertEqual(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateWaitForTLK, "CKKS re-entered waitfortlk");
1971 - (void)testRecoverFromBadCurrentKeyPointer {
1972 // The current key pointers in cloudkit shouldn't ever point to missing entries. But, if they do, CKKS must recover.
1974 // Test starts with a broken key hierarchy in our fake CloudKit, but the TLK already arrived.
1975 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1976 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1978 ZoneKeys* zonekeys = self.keys[self.keychainZoneID];
1979 FakeCKZone* ckzone = self.zones[self.keychainZoneID];
1980 ckzone.currentDatabase[zonekeys.currentTLKPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real tlk" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
1981 ckzone.currentDatabase[zonekeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class a key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
1982 ckzone.currentDatabase[zonekeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: @"not a real class c key" zoneID: self.keychainZoneID] action: CKReferenceActionNone];
1984 // Spin up CKKS subsystem.
1985 [self startCKKSSubsystem];
1987 // The CKKS subsystem should figure out the issue, and fix it.
1988 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
1990 [self.keychainView waitForKeyHierarchyReadiness];
1992 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1995 - (void)testRecoverFromCloudKitFetchFail {
1996 // Test starts with nothing in database, but one in our fake CloudKit.
1997 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1999 // The first two CKRecordZoneChanges should fail with a 'network unavailable' error.
2000 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
2001 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorNetworkUnavailable userInfo:@{}]];
2003 // Spin up CKKS subsystem.
2004 [self startCKKSSubsystem];
2006 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2007 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2009 // We expect a single record to be uploaded
2010 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2011 [self addGenericPassword: @"data" account: @"account-delete-me"];
2012 OCMVerifyAllWithDelay(self.mockDatabase, 12);
2014 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2015 [self addGenericPassword:@"asdf"
2016 account:@"account-class-A"
2018 access:(id)kSecAttrAccessibleWhenUnlocked
2019 expecting:errSecSuccess
2020 message:@"Adding class A item"];
2021 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2024 - (void)testRecoverFromCloudKitFetchFailWithDelay {
2025 // Test starts with nothing in database, but one in our fake CloudKit.
2026 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2028 // The first CKRecordZoneChanges should fail with a 'delay' error.
2029 self.silentFetchesAllowed = false;
2030 [self.keychainZone failNextFetchWith:[[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorRequestRateLimited userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:4]}]];
2031 [self expectCKFetch];
2033 // Spin up CKKS subsystem.
2034 [self startCKKSSubsystem];
2036 // Ensure it doesn't fetch within these three seconds (if it does, an exception will throw).
2039 // Okay, you can fetch again.
2040 [self expectCKFetch];
2042 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2043 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2045 // We expect a single record to be uploaded
2046 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2047 [self addGenericPassword: @"data" account: @"account-delete-me"];
2048 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2051 - (void)testRecoverFromCloudKitOldChangeToken {
2052 // Test starts with nothing in database, but one in our fake CloudKit.
2053 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2055 // Spin up CKKS subsystem.
2056 [self startCKKSSubsystem];
2058 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2059 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2061 // We expect a single record to be uploaded
2062 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2063 [self addGenericPassword: @"data" account: @"account-delete-me"];
2064 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2066 // Delete all old database states, to destroy the change tag validity
2067 [self.keychainZone.pastDatabases removeAllObjects];
2069 // We expect a total local flush and refetch
2070 self.silentFetchesAllowed = false;
2071 [self expectCKFetch]; // one to fail with a CKErrorChangeTokenExpired error
2072 [self expectCKFetch]; // and one to succeed
2074 // Trigger a fake change notification
2075 [self.keychainView notifyZoneChange:nil];
2077 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2079 // And check that a new upload happens just fine.
2080 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2081 [self addGenericPassword:@"asdf"
2082 account:@"account-class-A"
2084 access:(id)kSecAttrAccessibleWhenUnlocked
2085 expecting:errSecSuccess
2086 message:@"Adding class A item"];
2087 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2090 - (void)testRecoverFromCloudKitUnknownDeviceStateRecord {
2091 // Test starts with nothing in database, but one in our fake CloudKit.
2092 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2093 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2095 // Save a new device state record with some fake etag
2096 [self.keychainView dispatchSync: ^bool {
2097 CKKSDeviceStateEntry* cdse = [[CKKSDeviceStateEntry alloc] initForDevice:self.ckDeviceID
2098 circlePeerID:self.circlePeerID
2099 circleStatus:kSOSCCInCircle
2100 keyState:SecCKKSZoneKeyStateWaitForTLK
2102 currentClassAUUID:nil
2103 currentClassCUUID:nil
2104 zoneID:self.keychainZoneID
2105 encodedCKRecord:nil];
2106 XCTAssertNotNil(cdse, "Should have created a fake CDSE");
2107 CKRecord* record = [cdse CKRecordWithZoneID:self.keychainZoneID];
2108 XCTAssertNotNil(record, "Should have created a fake CDSE CKRecord");
2109 record.etag = @"fake etag";
2110 cdse.storedCKRecord = record;
2112 NSError* error = nil;
2113 [cdse saveToDatabase:&error];
2114 XCTAssertNil(error, @"No error saving cdse to database");
2119 // Spin up CKKS subsystem.
2120 [self startCKKSSubsystem];
2122 // We expect a record failure, since the device state record is broke
2123 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
2125 // And then we expect a clean write
2126 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
2127 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2129 [self addGenericPassword: @"data" account: @"account-delete-me"];
2130 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2133 - (void)testRecoverFromCloudKitUnknownItemRecord {
2134 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2136 // Spin up CKKS subsystem.
2137 [self startCKKSSubsystem];
2139 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
2141 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
2142 [self.keychainZone addToZone:ckr];
2144 [self.keychainView notifyZoneChange:nil];
2145 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2147 [self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
2149 // Delete the record from CloudKit, but miss the notification
2150 XCTAssertNil([self.keychainZone deleteCKRecordIDFromZone: ckr.recordID], "Deleting the record from fake CloudKit should succeed");
2152 // Expect a failed upload when we modify the item
2153 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
2154 [self updateGenericPassword:@"never seen again" account:@"account-delete-me"];
2155 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2157 [self.keychainView waitUntilAllOperationsAreFinished];
2159 // And the item should be disappeared from the local keychain
2160 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
2163 - (void)testRecoverFromCloudKitUserDeletedZone {
2164 // Test starts with nothing in database, but one in our fake CloudKit.
2165 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2167 // Spin up CKKS subsystem.
2168 [self startCKKSSubsystem];
2170 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2171 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2173 // We expect a single record to be uploaded
2174 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2175 [self addGenericPassword: @"data" account: @"account-delete-me"];
2176 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2178 // The first CKRecordZoneChanges should fail with a 'CKErrorUserDeletedZone' error.
2179 [self.keychainZone failNextFetchWith:[[NSError alloc] initWithDomain:CKErrorDomain code:CKErrorUserDeletedZone userInfo:@{}]];
2181 // We expect a key hierarchy upload, and then the class C item upload
2182 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
2183 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2185 [self.keychainView notifyZoneChange:nil];
2187 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2189 // And check that a new upload occurs.
2190 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2192 [self addGenericPassword:@"asdf"
2193 account:@"account-class-A"
2195 access:(id)kSecAttrAccessibleWhenUnlocked
2196 expecting:errSecSuccess
2197 message:@"Adding class A item"];
2198 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2201 - (void)testRecoverFromCloudKitZoneNotFound {
2202 // Test starts with nothing in database, but one in our fake CloudKit.
2203 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
2205 // Spin up CKKS subsystem.
2206 [self startCKKSSubsystem];
2208 // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
2209 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
2211 // We expect a single record to be uploaded
2212 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2213 [self addGenericPassword: @"data" account: @"account-delete-me"];
2214 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2216 // The next CKRecordZoneChanges should fail with a 'zone not found' error.
2217 NSError* zoneNotFoundError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
2218 code:CKErrorZoneNotFound
2220 NSError* error = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
2221 code:CKErrorPartialFailure
2222 userInfo:@{CKPartialErrorsByItemIDKey: @{self.keychainZoneID:zoneNotFoundError}}];
2223 [self.keychainZone failNextFetchWith:error];
2225 // We expect a key hierarchy upload, and then the class C item upload
2226 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
2227 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2229 [self.keychainView notifyZoneChange:nil];
2231 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2233 // And check that a new upload occurs.
2234 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
2236 [self addGenericPassword:@"asdf"
2237 account:@"account-class-A"
2239 access:(id)kSecAttrAccessibleWhenUnlocked
2240 expecting:errSecSuccess
2241 message:@"Adding class A item"];
2242 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2245 - (void)testNoCloudKitAccount {
2246 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
2247 self.accountStatus = CKAccountStatusNoAccount;
2248 self.circleStatus = kSOSCCNotInCircle;
2249 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2251 self.silentFetchesAllowed = false;
2252 [self startCKKSSubsystem];
2254 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2256 [self addGenericPassword: @"data" account: @"account-delete-me"];
2257 [self.keychainView waitUntilAllOperationsAreFinished];
2259 // simulate a NSNotification callback (but still logged out)
2260 self.accountStatus = CKAccountStatusNoAccount;
2261 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
2263 // There should be no further uploads, even when we save keychain items
2264 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
2265 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
2267 [self.keychainView waitUntilAllOperationsAreFinished];
2268 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2270 // Test that there are no items in the database (since we never logged in)
2271 [self checkNoCKKSData: self.keychainView];
2274 - (void)testSACloudKitAccount {
2275 // Test starts with nothing in database and the user logged into CloudKit and in circle, but the account is not HSA2.
2276 self.circleStatus = kSOSCCInCircle;
2277 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2279 self.accountStatus = CKAccountStatusAvailable;
2280 self.supportsDeviceToDeviceEncryption = NO;
2282 self.silentFetchesAllowed = false;
2283 [self startCKKSSubsystem];
2285 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2287 // There should be no uploads, even when we save keychain items and enter/exit circle
2288 [self addGenericPassword: @"data" account: @"account-delete-me"];
2289 [self.keychainView waitUntilAllOperationsAreFinished];
2291 self.circleStatus = kSOSCCNotInCircle;
2292 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2293 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
2295 self.circleStatus = kSOSCCInCircle;
2296 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2297 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
2299 [self.keychainView waitUntilAllOperationsAreFinished];
2300 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2302 // Test that there are no items in the database (since we never were in an HSA2 account)
2303 [self checkNoCKKSData: self.keychainView];
2306 - (void)testNoCircle {
2307 // Test starts with nothing in database and the user logged into CloudKit, but out of Circle.
2308 self.circleStatus = kSOSCCNotInCircle;
2309 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2311 self.accountStatus = CKAccountStatusAvailable;
2313 self.silentFetchesAllowed = false;
2314 [self startCKKSSubsystem];
2316 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2318 [self addGenericPassword: @"data" account: @"account-delete-me"];
2319 [self.keychainView waitUntilAllOperationsAreFinished];
2321 // simulate a NSNotification callback (but still logged out)
2322 self.accountStatus = CKAccountStatusNoAccount;
2323 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
2325 // There should be no further uploads, even when we save keychain items
2326 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
2327 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
2329 [self.keychainView waitUntilAllOperationsAreFinished];
2330 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2332 // Test that there are no items in the database (since we never logged in)
2333 [self checkNoCKKSData: self.keychainView];
2336 - (void)testCloudKitLogin {
2337 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
2338 self.accountStatus = CKAccountStatusNoAccount;
2339 self.circleStatus = kSOSCCNotInCircle;
2340 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2341 [self startCKKSSubsystem];
2343 [self.keychainView waitUntilAllOperationsAreFinished];
2344 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2346 // simulate a cloudkit login and NSNotification callback
2347 self.accountStatus = CKAccountStatusAvailable;
2348 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
2350 // No writes yet, since we're not in circle
2351 [self.keychainView waitUntilAllOperationsAreFinished];
2352 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2354 // We expect some sort of TLK/key hierarchy upload once we are notified of entering the circle.
2355 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
2357 self.circleStatus = kSOSCCInCircle;
2358 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2360 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2361 [self waitForCKModifications];
2363 // We expect a single class C record to be uploaded.
2364 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2365 [self addGenericPassword: @"data" account: @"account-delete-me"];
2367 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2368 [self waitForCKModifications];
2371 - (void)testCloudKitLogoutLogin {
2372 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
2373 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
2375 [self startCKKSSubsystem];
2377 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2378 [self waitForCKModifications];
2380 // We expect a single class C record to be uploaded.
2381 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2382 [self addGenericPassword: @"data" account: @"account-delete-me"];
2384 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2385 [self waitForCKModifications];
2387 // simulate a cloudkit logout and NSNotification callback
2388 self.accountStatus = CKAccountStatusNoAccount;
2389 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
2390 self.circleStatus = kSOSCCNotInCircle;
2391 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2393 // Test that there are no items in the database after logout
2394 [self.keychainView waitUntilAllOperationsAreFinished];
2395 [self checkNoCKKSData: self.keychainView];
2397 // There should be no further uploads, even when we save keychain items
2398 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
2399 [self addGenericPassword: @"data" account: @"account-delete-me-3"];
2401 [self.keychainView waitUntilAllOperationsAreFinished];
2402 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2404 // simulate a cloudkit login
2405 // 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
2406 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2408 self.accountStatus = CKAccountStatusAvailable;
2409 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
2410 self.circleStatus = kSOSCCInCircle;
2411 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2413 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2415 // Let everything settle...
2416 [self.keychainView waitUntilAllOperationsAreFinished];
2419 self.accountStatus = CKAccountStatusNoAccount;
2420 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
2421 self.circleStatus = kSOSCCNotInCircle;
2422 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2424 // Test that there are no items in the database after logout
2425 [self.keychainView waitUntilAllOperationsAreFinished];
2426 [self checkNoCKKSData: self.keychainView];
2428 // There should be no further uploads, even when we save keychain items
2429 [self addGenericPassword: @"data" account: @"account-delete-me-5"];
2430 [self addGenericPassword: @"data" account: @"account-delete-me-6"];
2432 [self.keychainView waitUntilAllOperationsAreFinished];
2433 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2435 // simulate a cloudkit login
2436 // 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
2437 [self expectCKModifyItemRecords: 2 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2439 self.accountStatus = CKAccountStatusAvailable;
2440 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
2441 self.circleStatus = kSOSCCInCircle;
2442 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2444 OCMVerifyAllWithDelay(self.mockDatabase, 20);
2447 - (void)testCloudKitLoginRace {
2448 // Test starts with nothing in database, and 'in circle', but securityd hasn't received notification if we're logged into CloudKit.
2449 // CKKS should not call handleLogout.
2451 id partialKVMock = OCMPartialMock(self.keychainView);
2452 OCMReject([partialKVMock handleCKLogout]);
2454 self.circleStatus = kSOSCCInCircle;
2455 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2456 [self startCKKSSubsystemOnly]; // note: don't unblock the ck account state object yet...
2458 // Add a keychain item, but make sure it doesn't upload yet.
2459 [self addGenericPassword: @"data" account: @"account-delete-me"];
2461 [self.keychainView waitUntilAllOperationsAreFinished];
2462 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2464 // Now that we're here (and handleCKLogout hasn't been called), bring the account up
2466 // We expect some sort of TLK/key hierarchy upload once we are notified of entering the circle.
2467 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 zoneID:self.keychainZoneID];
2469 // We expect a single class C record to be uploaded.
2470 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2472 self.accountStatus = CKAccountStatusAvailable;
2473 [self startCKAccountStatusMock];
2475 // simulate another NSNotification callback
2476 [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];
2478 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2479 [self waitForCKModifications];
2481 // Make sure new items upload too
2482 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
2483 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
2484 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2486 [self.keychainView waitUntilAllOperationsAreFinished];
2487 [self waitForCKModifications];
2488 [self.keychainView cancelAllOperations];
2490 [partialKVMock stopMocking];
2493 - (void)testDeviceStateUploadGood {
2494 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2496 [self startCKKSSubsystem];
2497 [self.keychainView waitForKeyHierarchyReadiness];
2499 __weak __typeof(self) weakSelf = self;
2500 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
2501 deletedRecordTypeCounts:nil
2502 zoneID:self.keychainZoneID
2503 checkModifiedRecord: ^BOOL (CKRecord* record){
2504 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
2505 // Check that all the things matches
2506 __strong __typeof(weakSelf) strongSelf = weakSelf;
2507 XCTAssertNotNil(strongSelf, "self exists");
2509 ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
2510 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
2512 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], @"fake-circle-id", "peer ID matches what we gave it");
2513 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
2514 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateReady), "Device is in ready");
2516 XCTAssertEqualObjects([record[SecCKRecordCurrentTLK] recordID].recordName, zoneKeys.tlk.uuid, "Correct TLK uuid");
2517 XCTAssertEqualObjects([record[SecCKRecordCurrentClassA] recordID].recordName, zoneKeys.classA.uuid, "Correct class A uuid");
2518 XCTAssertEqualObjects([record[SecCKRecordCurrentClassC] recordID].recordName, zoneKeys.classC.uuid, "Correct class C uuid");
2524 runAfterModification:nil];
2526 [self.keychainView updateDeviceState:false ckoperationGroup:nil];
2528 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2531 - (void)testDeviceStateUploadRateLimited {
2532 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2534 [self startCKKSSubsystem];
2535 [self.keychainView waitForKeyHierarchyReadiness];
2537 __weak __typeof(self) weakSelf = self;
2538 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
2539 deletedRecordTypeCounts:nil
2540 zoneID:self.keychainZoneID
2541 checkModifiedRecord: ^BOOL (CKRecord* record){
2542 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
2543 // Check that all the things matches
2544 __strong __typeof(weakSelf) strongSelf = weakSelf;
2545 XCTAssertNotNil(strongSelf, "self exists");
2547 ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
2548 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
2550 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], @"fake-circle-id", "peer ID matches what we gave it");
2551 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
2552 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateReady), "Device is in ready");
2554 XCTAssertEqualObjects([record[SecCKRecordCurrentTLK] recordID].recordName, zoneKeys.tlk.uuid, "Correct TLK uuid");
2555 XCTAssertEqualObjects([record[SecCKRecordCurrentClassA] recordID].recordName, zoneKeys.classA.uuid, "Correct class A uuid");
2556 XCTAssertEqualObjects([record[SecCKRecordCurrentClassC] recordID].recordName, zoneKeys.classC.uuid, "Correct class C uuid");
2562 runAfterModification:nil];
2564 CKKSUpdateDeviceStateOperation* op = [self.keychainView updateDeviceState:true ckoperationGroup:nil];
2565 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2566 [op waitUntilFinished];
2568 // Check that an immediate rate-limited retry doesn't upload anything
2569 op = [self.keychainView updateDeviceState:true ckoperationGroup:nil];
2570 [op waitUntilFinished];
2572 // But not rate-limiting works just fine!
2573 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
2574 deletedRecordTypeCounts:nil
2575 zoneID:self.keychainZoneID
2576 checkModifiedRecord:nil
2577 runAfterModification:nil];
2578 op = [self.keychainView updateDeviceState:false ckoperationGroup:nil];
2579 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2580 [op waitUntilFinished];
2582 // And now, if the update is old enough, that'll work too
2583 [self.keychainView dispatchSync:^bool {
2584 NSError* error = nil;
2585 CKKSDeviceStateEntry* cdse = [CKKSDeviceStateEntry fromDatabase:self.accountStateTracker.ckdeviceID zoneID:self.keychainZoneID error:&error];
2586 XCTAssertNil(error, "No error fetching device state entry");
2587 XCTAssertNotNil(cdse, "Fetched device state entry");
2589 CKRecord* record = cdse.storedCKRecord;
2591 NSDate* m = record.modificationDate;
2592 XCTAssertNotNil(m, "Have modification date");
2595 NSDateComponents* offset = [[NSDateComponents alloc] init];
2596 [offset setHour:-4 * 24];
2597 NSDate* m2 = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:m options:0];
2599 XCTAssertNotNil(m2, "Made modification date");
2601 record.modificationDate = m2;
2602 [cdse setStoredCKRecord:record];
2604 [cdse saveToDatabase:&error];
2605 XCTAssertNil(error, "No error saving device state entry");
2610 // And now the rate-limiting doesn't get in the way
2611 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
2612 deletedRecordTypeCounts:nil
2613 zoneID:self.keychainZoneID
2614 checkModifiedRecord:nil
2615 runAfterModification:nil];
2616 op = [self.keychainView updateDeviceState:true ckoperationGroup:nil];
2617 OCMVerifyAllWithDelay(self.mockDatabase, 12);
2618 [op waitUntilFinished];
2621 - (void)testDeviceStateUploadRateLimitedAfterNormalUpload {
2622 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2624 [self startCKKSSubsystem];
2625 [self.keychainView waitForKeyHierarchyReadiness];
2627 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
2628 [self addGenericPassword:@"password" account:@"account-delete-me"];
2629 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2631 // Check that an immediate rate-limited retry doesn't upload anything
2632 CKKSUpdateDeviceStateOperation* op = [self.keychainView updateDeviceState:true ckoperationGroup:nil];
2633 [op waitUntilFinished];
2636 - (void)testDeviceStateReceive {
2637 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
2639 ZoneKeys* zoneKeys = self.keys[self.keychainZoneID];
2640 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", self.keychainZoneID);
2642 [self startCKKSSubsystem];
2643 [self.keychainView waitForKeyHierarchyReadiness];
2645 CKKSDeviceStateEntry* cdse = [[CKKSDeviceStateEntry alloc] initForDevice:@"otherdevice"
2646 circlePeerID:@"asdfasdf"
2647 circleStatus:kSOSCCInCircle
2648 keyState:SecCKKSZoneKeyStateReady
2649 currentTLKUUID:zoneKeys.tlk.uuid
2650 currentClassAUUID:zoneKeys.classA.uuid
2651 currentClassCUUID:zoneKeys.classC.uuid
2652 zoneID:self.keychainZoneID
2653 encodedCKRecord:nil];
2654 CKRecord* record = [cdse CKRecordWithZoneID:self.keychainZoneID];
2655 [self.keychainZone addToZone:record];
2657 // Trigger a notification (with hilariously fake data)
2658 [self.keychainView notifyZoneChange:nil];
2659 [self.keychainView waitForFetchAndIncomingQueueProcessing];
2661 [self.keychainView dispatchSync: ^bool {
2662 NSError* error = nil;
2663 NSArray<CKKSDeviceStateEntry*>* cdses = [CKKSDeviceStateEntry allInZone:self.keychainZoneID error:&error];
2664 XCTAssertNil(error, "No error fetching CDSEs");
2665 XCTAssertNotNil(cdses, "An array of CDSEs was returned");
2666 XCTAssert(cdses.count >= 1u, "At least one CDSE came back");
2668 CKKSDeviceStateEntry* item = nil;
2669 for(CKKSDeviceStateEntry* dbcdse in cdses) {
2670 if([dbcdse.device isEqualToString:@"otherdevice"]) {
2674 XCTAssertNotNil(item, "Found a cdse for otherdevice");
2676 XCTAssertEqualObjects(cdse, item, "Saved item matches pre-cloudkit item");
2678 XCTAssertEqualObjects(item.circlePeerID, @"asdfasdf", "correct peer id");
2679 XCTAssertEqualObjects(item.keyState, SecCKKSZoneKeyStateReady, "correct key state");
2680 XCTAssertEqualObjects(item.currentTLKUUID, zoneKeys.tlk.uuid, "correct tlk uuid");
2681 XCTAssertEqualObjects(item.currentClassAUUID, zoneKeys.classA.uuid, "correct classA uuid");
2682 XCTAssertEqualObjects(item.currentClassCUUID, zoneKeys.classC.uuid, "correct classC uuid");
2687 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2690 - (void)testDeviceStateUploadBadKeyState {
2691 // This test has stuff in CloudKit, but no TLKs. It should become very sad.
2692 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
2694 [self startCKKSSubsystem];
2695 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
2696 XCTAssertEqual(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateWaitForTLK, "CKKS entered waitfortlk");
2698 __weak __typeof(self) weakSelf = self;
2699 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
2700 deletedRecordTypeCounts:nil
2701 zoneID:self.keychainZoneID
2702 checkModifiedRecord: ^BOOL (CKRecord* record){
2703 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
2704 // Check that all the things matches
2705 __strong __typeof(weakSelf) strongSelf = weakSelf;
2706 XCTAssertNotNil(strongSelf, "self exists");
2708 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], @"fake-circle-id", "peer ID matches what we gave it");
2709 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
2710 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTLK), "Device is in waitfortlk");
2712 XCTAssertNil(record[SecCKRecordCurrentTLK] , "No TLK");
2713 XCTAssertNil(record[SecCKRecordCurrentClassA], "No class A key");
2714 XCTAssertNil(record[SecCKRecordCurrentClassC], "No class C key");
2720 runAfterModification:nil];
2722 [self.keychainView updateDeviceState:false ckoperationGroup:nil];
2724 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2727 - (void)testDeviceStateUploadBadCircleState {
2728 self.circleStatus = kSOSCCNotInCircle;
2729 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
2731 // This test has stuff in CloudKit, but no TLKs.
2732 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
2734 [self startCKKSSubsystem];
2736 // Since CKKS should start up enough to get back into the error state and then back into initializing state, wait for that to happen.
2737 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateError] wait:8*NSEC_PER_SEC], "CKKS entered error");
2738 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateInitializing] wait:8*NSEC_PER_SEC], "CKKS entered initializing");
2739 XCTAssertEqual(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateInitializing, "CKKS entered intializing");
2741 __weak __typeof(self) weakSelf = self;
2742 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
2743 deletedRecordTypeCounts:nil
2744 zoneID:self.keychainZoneID
2745 checkModifiedRecord: ^BOOL (CKRecord* record){
2746 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
2747 // Check that all the things matches
2748 __strong __typeof(weakSelf) strongSelf = weakSelf;
2749 XCTAssertNotNil(strongSelf, "self exists");
2751 XCTAssertNil(record[SecCKRecordCirclePeerID], "no peer ID if device is not in circle");
2752 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCNotInCircle], "device is not in circle");
2753 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateInitializing), "Device is in keystate:initializing");
2755 XCTAssertNil(record[SecCKRecordCurrentTLK] , "No TLK");
2756 XCTAssertNil(record[SecCKRecordCurrentClassA], "No class A key");
2757 XCTAssertNil(record[SecCKRecordCurrentClassC], "No class C key");
2763 runAfterModification:nil];
2765 CKKSUpdateDeviceStateOperation* op = [self.keychainView updateDeviceState:false ckoperationGroup:nil];
2766 OCMVerifyAllWithDelay(self.mockDatabase, 8);
2768 [op waitUntilFinished];
2769 XCTAssertNil(op.error, "No error uploading 'out of circle' device state");
2772 - (void)testCKKSControlBringup {
2773 xpc_endpoint_t endpoint = SecServerCreateCKKSEndpoint();
2774 XCTAssertNotNil(endpoint, "Received endpoint");
2776 NSXPCInterface *interface = CKKSSetupControlProtocol([NSXPCInterface interfaceWithProtocol:@protocol(CKKSControlProtocol)]);
2777 XCTAssertNotNil(interface, "Received a configured CKKS interface");
2779 NSXPCListenerEndpoint *listenerEndpoint = [[NSXPCListenerEndpoint alloc] init];
2780 [listenerEndpoint _setEndpoint:endpoint];
2782 NSXPCConnection* connection = [[NSXPCConnection alloc] initWithListenerEndpoint:listenerEndpoint];
2783 XCTAssertNotNil(connection , "Received an active connection");
2785 connection.remoteObjectInterface = interface;