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@
26 #import <CloudKit/CloudKit.h>
27 #import <XCTest/XCTest.h>
28 #import <OCMock/OCMock.h>
30 #include <Security/SecItemPriv.h>
31 #include "OSX/sec/Security/SecItemShim.h"
33 #include <Security/SecEntitlements.h>
34 #include <ipc/server_security_helpers.h>
35 #import <Foundation/NSXPCConnection_Private.h>
37 #import "keychain/categories/NSError+UsefulConstructors.h"
39 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
40 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
41 #import "keychain/ckks/CKKS.h"
42 #import "keychain/ckks/CKKSItem.h"
43 #import "keychain/ckks/CKKSItemEncrypter.h"
44 #import "keychain/ckks/CKKSKey.h"
45 #import "keychain/ckks/CKKSViewManager.h"
46 #import "keychain/ckks/CKKSZoneStateEntry.h"
48 #import "keychain/ckks/CKKSControl.h"
49 #import "keychain/ckks/CloudKitCategories.h"
51 #import "keychain/ckks/tests/MockCloudKit.h"
52 #import "keychain/ckks/tests/CKKSTests.h"
53 #import "keychain/ckks/tests/CKKSTests+API.h"
55 @implementation CloudKitKeychainSyncingTestsBase (APITests)
57 -(NSMutableDictionary*)pcsAddItemQuery:(NSString*)account
59 serviceIdentifier:(NSNumber*)serviceIdentifier
60 publicKey:(NSData*)publicKey
61 publicIdentity:(NSData*)publicIdentity
64 (id)kSecClass : (id)kSecClassGenericPassword,
65 (id)kSecReturnPersistentRef: @YES,
66 (id)kSecReturnAttributes: @YES,
67 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
68 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
69 (id)kSecAttrAccount : account,
70 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
71 (id)kSecValueData : data,
72 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
73 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
74 (id)kSecAttrPCSPlaintextServiceIdentifier : serviceIdentifier,
75 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
76 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
80 -(NSDictionary*)pcsAddItem:(NSString*)account
82 serviceIdentifier:(NSNumber*)serviceIdentifier
83 publicKey:(NSData*)publicKey
84 publicIdentity:(NSData*)publicIdentity
85 expectingSync:(bool)expectingSync
87 NSMutableDictionary* query = [self pcsAddItemQuery:account
89 serviceIdentifier:(NSNumber*)serviceIdentifier
90 publicKey:(NSData*)publicKey
91 publicIdentity:(NSData*)publicIdentity];
92 CFTypeRef result = NULL;
93 XCTestExpectation* syncExpectation = [self expectationWithDescription: @"callback occurs"];
95 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, &result, ^(bool didSync, CFErrorRef error) {
97 XCTAssertTrue(didSync, "Item synced");
98 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
100 XCTAssertFalse(didSync, "Item did not sync");
101 XCTAssertNotNil((__bridge NSError*)error, "Error syncing item");
104 [syncExpectation fulfill];
105 }), @"_SecItemAddAndNotifyOnSync succeeded");
107 // Verify that the item was written to CloudKit
108 OCMVerifyAllWithDelay(self.mockDatabase, 20);
110 // In real code, you'd need to wait for the _SecItemAddAndNotifyOnSync callback to succeed before proceeding
111 [self waitForExpectations:@[syncExpectation] timeout:20];
113 return (NSDictionary*) CFBridgingRelease(result);
116 - (BOOL (^) (CKRecord*)) checkPCSFieldsBlock: (CKRecordZoneID*) zoneID
117 PCSServiceIdentifier:(NSNumber*)servIdentifier
118 PCSPublicKey:(NSData*)publicKey
119 PCSPublicIdentity:(NSData*)publicIdentity
121 __weak __typeof(self) weakSelf = self;
122 return ^BOOL(CKRecord* record) {
123 __strong __typeof(weakSelf) strongSelf = weakSelf;
124 XCTAssertNotNil(strongSelf, "self exists");
126 XCTAssert([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier], "PCS Service identifier matches input");
127 XCTAssert([record[SecCKRecordPCSPublicKey] isEqual: publicKey], "PCS Public Key matches input");
128 XCTAssert([record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity], "PCS Public Identity matches input");
130 if([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier] &&
131 [record[SecCKRecordPCSPublicKey] isEqual: publicKey] &&
132 [record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity]) {
141 @interface CloudKitKeychainSyncingAPITests : CloudKitKeychainSyncingTestsBase
144 @implementation CloudKitKeychainSyncingAPITests
145 - (void)testSecuritydClientBringup {
147 CFErrorRef cferror = nil;
148 xpc_endpoint_t endpoint = SecCreateSecuritydXPCServerEndpoint(&cferror);
149 XCTAssertNil((__bridge id)cferror, "No error creating securityd endpoint");
150 XCTAssertNotNil(endpoint, "Received securityd endpoint");
153 NSXPCInterface *interface = [NSXPCInterface interfaceWithProtocol:@protocol(SecuritydXPCProtocol)];
154 [SecuritydXPCClient configureSecuritydXPCProtocol: interface];
155 XCTAssertNotNil(interface, "Received a configured CKKS interface");
158 NSXPCListenerEndpoint *listenerEndpoint = [[NSXPCListenerEndpoint alloc] init];
159 [listenerEndpoint _setEndpoint:endpoint];
161 NSXPCConnection* connection = [[NSXPCConnection alloc] initWithListenerEndpoint:listenerEndpoint];
162 XCTAssertNotNil(connection , "Received an active connection");
164 connection.remoteObjectInterface = interface;
168 - (void)testAddAndNotifyOnSync {
169 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
171 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
172 [self startCKKSSubsystem];
174 // Let things shake themselves out.
175 [self.keychainView waitForKeyHierarchyReadiness];
176 [self waitForCKModifications];
178 NSMutableDictionary* query = [@{
179 (id)kSecClass : (id)kSecClassGenericPassword,
180 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
181 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
182 (id)kSecAttrAccount : @"testaccount",
183 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
184 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
185 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
188 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
190 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
191 XCTAssertTrue(didSync, "Item synced properly");
192 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
194 [blockExpectation fulfill];
195 }), @"_SecItemAddAndNotifyOnSync succeeded");
197 OCMVerifyAllWithDelay(self.mockDatabase, 10);
199 [self waitForExpectationsWithTimeout:5.0 handler:nil];
202 - (void)testAddAndNotifyOnSyncSkipsQueue {
203 // Use the PCS plaintext fields to determine which object is which
204 SecResetLocalSecuritydXPCFakeEntitlements();
205 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
206 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
207 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
209 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
211 [self startCKKSSubsystem];
212 [self.keychainView waitForKeyHierarchyReadiness];
213 [self waitForCKModifications];
214 OCMVerifyAllWithDelay(self.mockDatabase, 40);
216 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
217 self.keychainView.holdOutgoingQueueOperation = [CKKSGroupOperation named:@"outgoing-hold" withBlock: ^{
218 ckksnotice_global("ckks", "releasing outgoing-queue hold");
221 for(size_t count = 0; count < 150; count++) {
222 [self addGenericPassword:@"data" account:[NSString stringWithFormat:@"account-delete-me-%03lu", count]];
225 NSMutableDictionary* query = [@{
226 (id)kSecClass : (id)kSecClassGenericPassword,
227 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
228 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
229 (id)kSecAttrAccount : @"testaccount",
230 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
231 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
232 (id)kSecAttrPCSPlaintextPublicKey : [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
233 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
236 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
238 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
239 XCTAssertTrue(didSync, "Item synced properly");
240 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
242 [blockExpectation fulfill];
243 }), @"_SecItemAddAndNotifyOnSync succeeded");
246 XCTestExpectation* firstQueueOperation = [self expectationWithDescription: @"found the item in the first queue iteration"];
247 [self expectCKModifyItemRecords:SecCKKSOutgoingQueueItemsAtOnce
248 currentKeyPointerRecords:1
249 zoneID:self.keychainZoneID
250 checkItem:^BOOL(CKRecord * _Nonnull record) {
251 if(record[SecCKRecordPCSPublicKey]) {
252 [firstQueueOperation fulfill];
256 [self expectCKModifyItemRecords:51 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
258 // Release the hounds
259 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
261 [self waitForExpectations:@[blockExpectation, firstQueueOperation] timeout:20];
262 OCMVerifyAllWithDelay(self.mockDatabase, 10);
265 - (void)testAddAndNotifyOnSyncFailure {
266 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
268 [self startCKKSSubsystem];
269 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
271 [self.keychainView waitForFetchAndIncomingQueueProcessing];
273 // Due to item UUID selection, this item will be added with UUID 50184A35-4480-E8BA-769B-567CF72F1EC0.
274 // Add it to CloudKit first!
275 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"50184A35-4480-E8BA-769B-567CF72F1EC0"];
276 [self.keychainZone addToZone: ckr];
280 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
282 NSMutableDictionary* query = [@{
283 (id)kSecClass : (id)kSecClassGenericPassword,
284 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
285 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
286 (id)kSecAttrAccount : @"testaccount",
287 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
288 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
289 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
290 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // @ fake view hint for fake view
293 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
295 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
296 XCTAssertFalse(didSync, "Item did not sync (as expected)");
297 XCTAssertNotNil((__bridge NSError*)error, "error exists when item fails to sync");
299 [blockExpectation fulfill];
300 }), @"_SecItemAddAndNotifyOnSync succeeded");
302 [self waitForExpectationsWithTimeout:5.0 handler:nil];
303 [self waitForCKModifications];
306 - (void)testAddAndNotifyOnSyncLoggedOut {
307 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
308 self.accountStatus = CKAccountStatusNoAccount;
309 self.silentFetchesAllowed = false;
310 [self startCKKSSubsystem];
312 XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "CKKS should positively log out");
314 NSMutableDictionary* query = [@{
315 (id)kSecClass : (id)kSecClassGenericPassword,
316 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
317 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
318 (id)kSecAttrAccount : @"testaccount",
319 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
320 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
321 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
324 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
326 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
327 XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)");
328 XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out");
330 [blockExpectation fulfill];
331 }), @"_SecItemAddAndNotifyOnSync succeeded");
333 [self waitForExpectationsWithTimeout:5.0 handler:nil];
336 - (void)testAddAndNotifyOnSyncAccountStatusUnclear {
337 // Test starts with nothing in database, but CKKS hasn't been told we've logged out yet.
338 // We expect no CKKS operations.
339 self.accountStatus = CKAccountStatusNoAccount;
340 self.silentFetchesAllowed = false;
342 NSMutableDictionary* query = [@{
343 (id)kSecClass : (id)kSecClassGenericPassword,
344 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
345 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
346 (id)kSecAttrAccount : @"testaccount",
347 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
348 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
349 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
352 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
354 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
355 XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)");
356 XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out");
358 [blockExpectation fulfill];
359 }), @"_SecItemAddAndNotifyOnSync succeeded");
361 // And now, allow CKKS to discover we're logged out
362 [self startCKKSSubsystem];
363 XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "CKKS should positively log out");
365 [self waitForExpectationsWithTimeout:5.0 handler:nil];
368 - (void)testAddAndNotifyOnSyncBeforeKeyHierarchyReady {
369 // Test starts with a key hierarchy in cloudkit and the TLK having arrived
370 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
371 [self saveTLKMaterialToKeychain:self.keychainZoneID];
372 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
374 // But block CloudKit fetches (so the key hierarchy won't be ready when we add this new item)
375 [self holdCloudKitFetches];
377 [self startCKKSSubsystem];
378 XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "CKKS should log in");
379 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateFetch] wait:20*NSEC_PER_SEC], @"Should have reached key state 'fetch', but no further");
381 NSMutableDictionary* query = [@{
382 (id)kSecClass : (id)kSecClassGenericPassword,
383 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
384 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
385 (id)kSecAttrAccount : @"testaccount",
386 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
387 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
388 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
391 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
393 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
394 XCTAssertTrue(didSync, "Item synced");
395 XCTAssertNil((__bridge NSError*)error, "Shouldn't have received an error syncing item");
397 [blockExpectation fulfill];
398 }), @"_SecItemAddAndNotifyOnSync succeeded");
400 // We should be in the 'fetch' state, but no further
401 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateFetch] wait:20*NSEC_PER_SEC], @"Should have reached key state 'fetch', but no further");
403 // When we release the fetch, the callback should still fire and the item should upload
404 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
405 [self releaseCloudKitFetchHold];
406 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Should have reached key state 'ready'");
408 // Verify that the item was written to CloudKit
409 OCMVerifyAllWithDelay(self.mockDatabase, 20);
411 [self waitForExpectationsWithTimeout:5.0 handler:nil];
414 - (void)testAddAndNotifyOnSyncBeforePolicyLoaded {
415 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
416 [self saveTLKMaterialToKeychain:self.keychainZoneID];
417 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
419 [self startCKKSSubsystem];
421 // Allow CKKS to spin up fully
422 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
424 [self.keychainView waitForOperationsOfClass:[CKKSScanLocalItemsOperation class]];
425 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
427 // Simulate a daemon restart
428 self.automaticallyBeginCKKSViewCloudKitOperation = false;
429 [self.injectedManager resetSyncingPolicy];
430 [self.injectedManager haltZone:self.keychainZoneID.zoneName];
432 // Issue the query (to simulate the query starting securityd)
433 NSMutableDictionary* query = [@{
434 (id)kSecClass : (id)kSecClassGenericPassword,
435 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
436 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
437 (id)kSecAttrAccount : @"testaccount",
438 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
439 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
440 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
443 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
445 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
446 XCTAssertTrue(didSync, "Item synced");
447 XCTAssertNil((__bridge NSError*)error, "Shouldn't have received an error syncing item");
449 [blockExpectation fulfill];
450 }), @"_SecItemAddAndNotifyOnSync succeeded");
452 // When the policy is loaded, the item should upload and the callback should fire
453 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
455 [self.injectedManager setCurrentSyncingPolicy:self.viewSortingPolicyForManagedViewList];
456 self.keychainView = [self.injectedManager findView:self.keychainZoneID.zoneName];
458 [self.injectedManager beginCloudKitOperationOfAllViews];
459 [self beginSOSTrustedViewOperation:self.keychainView];
461 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Should have reached key state 'ready'");
462 OCMVerifyAllWithDelay(self.mockDatabase, 20);
464 [self waitForExpectations:@[blockExpectation] timeout:5];
467 - (void)testAddAndNotifyOnSyncReaddAtSameUUIDAfterDeleteItem {
468 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID]; // Make life easy for this test.
470 __block CKRecordID* itemRecordID = nil;
471 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:^BOOL(CKRecord * _Nonnull record) {
472 itemRecordID = record.recordID;
476 [self startCKKSSubsystem];
478 // Let things shake themselves out.
479 [self.keychainView waitForKeyHierarchyReadiness];
480 [self waitForCKModifications];
482 NSMutableDictionary* query = [@{
483 (id)kSecClass : (id)kSecClassGenericPassword,
484 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
485 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
486 (id)kSecAttrAccount : @"testaccount",
487 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
488 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
489 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
492 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
494 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
495 XCTAssertTrue(didSync, "Item synced properly");
496 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
498 [blockExpectation fulfill];
499 }), @"_SecItemAddAndNotifyOnSync succeeded");
501 OCMVerifyAllWithDelay(self.mockDatabase, 10);
503 [self waitForExpectations:@[blockExpectation] timeout:5];
505 // And the item is deleted
506 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
507 [self deleteGenericPassword:@"testaccount"];
508 OCMVerifyAllWithDelay(self.mockDatabase, 20);
510 // And the item is readded. It should come back to its previous UUID.
511 XCTAssertNotNil(itemRecordID, "Should have an item record ID");
512 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:^BOOL(CKRecord * _Nonnull record) {
513 XCTAssertEqualObjects(itemRecordID.recordName, record.recordID.recordName, "Uploaded item UUID should match previous upload");
516 XCTestExpectation* blockExpectation2 = [self expectationWithDescription: @"callback occurs"];
517 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
518 XCTAssertTrue(didSync, "Item synced properly");
519 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
521 [blockExpectation2 fulfill];
522 }), @"_SecItemAddAndNotifyOnSync succeeded");
524 OCMVerifyAllWithDelay(self.mockDatabase, 10);
525 [self waitForExpectations:@[blockExpectation2] timeout:5];
528 - (void)testPCSUnencryptedFieldsAdd {
529 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
531 [self startCKKSSubsystem];
532 [self.keychainView waitForKeyHierarchyReadiness];
534 NSNumber* servIdentifier = @3;
535 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
536 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
538 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
539 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
540 PCSServiceIdentifier:(NSNumber *)servIdentifier
541 PCSPublicKey:publicKey
542 PCSPublicIdentity:publicIdentity]];
544 NSMutableDictionary* query = [@{
545 (id)kSecClass : (id)kSecClassGenericPassword,
546 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
547 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
548 (id)kSecAttrAccount : @"testaccount",
549 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
550 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
551 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
552 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
553 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
554 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
555 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // allows a CKKSScanOperation to find this item
558 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
560 // Verify that the item is written to CloudKit
561 OCMVerifyAllWithDelay(self.mockDatabase, 20);
563 CFTypeRef item = NULL;
564 query[(id)kSecValueData] = nil;
565 query[(id)kSecReturnAttributes] = @YES;
566 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
568 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
569 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Service Identifier exists");
570 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key exists");
571 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity exists");
573 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
574 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
575 [self waitForCKModifications];
576 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
577 CKRecord* record = self.keychainZone.currentDatabase[recordID];
578 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
580 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], servIdentifier, "Service identifier sent to cloudkit");
581 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], publicKey, "public key sent to cloudkit");
582 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], publicIdentity, "public identity sent to cloudkit");
585 - (void)testPCSUnencryptedFieldsModify {
586 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
588 [self startCKKSSubsystem];
589 [self.keychainView waitForKeyHierarchyReadiness];
591 NSNumber* servIdentifier = @3;
592 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
593 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
595 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
596 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
597 PCSServiceIdentifier:(NSNumber *)servIdentifier
598 PCSPublicKey:publicKey
599 PCSPublicIdentity:publicIdentity]];
601 NSMutableDictionary* query = [@{
602 (id)kSecClass : (id)kSecClassGenericPassword,
603 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
604 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
605 (id)kSecAttrAccount : @"testaccount",
606 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
607 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
608 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
609 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
610 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
611 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
612 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // allows a CKKSScanOperation to find this item
615 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
617 OCMVerifyAllWithDelay(self.mockDatabase, 20);
618 [self waitForCKModifications];
620 query[(id)kSecValueData] = nil;
621 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
622 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
623 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
626 publicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
628 NSNumber* newServiceIdentifier = @10;
629 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
630 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
632 NSDictionary* update = @{
633 (id)kSecAttrPCSPlaintextServiceIdentifier : newServiceIdentifier,
634 (id)kSecAttrPCSPlaintextPublicKey : newPublicKey,
635 (id)kSecAttrPCSPlaintextPublicIdentity : newPublicIdentity,
638 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
639 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
640 PCSServiceIdentifier:(NSNumber *)newServiceIdentifier
641 PCSPublicKey:newPublicKey
642 PCSPublicIdentity:newPublicIdentity]];
644 XCTAssertEqual(errSecSuccess, SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update), @"SecItemUpdate succeeded");
645 OCMVerifyAllWithDelay(self.mockDatabase, 20);
647 CFTypeRef item = NULL;
648 query[(id)kSecValueData] = nil;
649 query[(id)kSecReturnAttributes] = @YES;
650 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
652 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
653 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], newServiceIdentifier, "Service Identifier exists");
654 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], newPublicKey, "public key exists");
655 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], newPublicIdentity, "public identity exists");
657 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
658 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
659 [self waitForCKModifications];
660 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
661 CKRecord* record = self.keychainZone.currentDatabase[recordID];
662 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
664 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], newServiceIdentifier, "Service identifier sent to cloudkit");
665 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], newPublicKey, "public key sent to cloudkit");
666 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], newPublicIdentity, "public identity sent to cloudkit");
669 // As of [<rdar://problem/32558310> CKKS: Re-authenticate PCSPublicFields], these fields are NOT server-modifiable. This test proves it.
670 - (void)testPCSUnencryptedFieldsServerModifyFail {
671 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
673 [self startCKKSSubsystem];
674 [self.keychainView waitForKeyHierarchyReadiness];
676 NSNumber* servIdentifier = @3;
677 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
678 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
680 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
681 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
682 PCSServiceIdentifier:(NSNumber *)servIdentifier
683 PCSPublicKey:publicKey
684 PCSPublicIdentity:publicIdentity]];
686 NSMutableDictionary* query = [@{
687 (id)kSecClass : (id)kSecClassGenericPassword,
688 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
689 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
690 (id)kSecAttrAccount : @"testaccount",
691 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
692 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
693 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
694 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
695 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
696 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
697 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // fake, for CKKSScanOperation
700 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
702 OCMVerifyAllWithDelay(self.mockDatabase, 20);
703 [self waitForCKModifications];
705 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
706 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
707 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
708 CKRecord* record = self.keychainZone.currentDatabase[recordID];
709 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
711 // Items are encrypted using encv2
712 XCTAssertEqualObjects(record[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
715 // Test has already failed; find the record just to be nice.
716 for(CKRecord* maybe in self.keychainZone.currentDatabase.allValues) {
717 if(maybe[SecCKRecordPCSServiceIdentifier] != nil) {
723 NSNumber* newServiceIdentifier = @10;
724 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
725 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
727 // Change the public key and public identity
728 record = [record copyWithZone: nil];
729 record[SecCKRecordPCSServiceIdentifier] = newServiceIdentifier;
730 record[SecCKRecordPCSPublicKey] = newPublicKey;
731 record[SecCKRecordPCSPublicIdentity] = newPublicIdentity;
732 [self.keychainZone addToZone: record];
734 // Trigger a notification
735 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
736 [self.keychainView waitForFetchAndIncomingQueueProcessing];
738 CFTypeRef item = NULL;
739 query[(id)kSecValueData] = nil;
740 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
741 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
742 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
743 query[(id)kSecReturnAttributes] = @YES;
744 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
746 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
747 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "service identifier is not updated");
748 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key not updated");
749 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity not updated");
752 -(void)testPCSUnencryptedFieldsRecieveUnauthenticatedFields {
753 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
755 [self startCKKSSubsystem];
756 [self.keychainView waitForKeyHierarchyReadiness];
758 NSNumber* servIdentifier = @3;
759 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
760 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
762 NSError* error = nil;
764 // Manually encrypt an item
765 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
766 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
767 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
768 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
769 parentKeyUUID:self.keychainZoneKeys.classC.uuid
770 zoneID:recordID.zoneID];
771 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
772 XCTAssertNotNil(itemkey, "Got a key");
773 cipheritem.wrappedkey = itemkey.wrappedkey;
774 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
776 cipheritem.encver = CKKSItemEncryptionVersion1;
778 // This item has the PCS public fields, but they are not authenticated
779 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
780 cipheritem.plaintextPCSPublicKey = publicKey;
781 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
783 NSDictionary<NSString*, NSData*>* authenticatedData = [cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion1];
784 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
785 XCTAssertNil(error, "no error encrypting object");
786 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
788 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
790 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
791 [self.keychainView waitForFetchAndIncomingQueueProcessing];
793 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
794 (id)kSecReturnAttributes: @YES,
795 (id)kSecAttrSynchronizable: @YES,
796 (id)kSecAttrAccount: @"account-delete-me",
797 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
799 CFTypeRef cfresult = NULL;
800 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
802 NSDictionary* result = CFBridgingRelease(cfresult);
803 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
804 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
805 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
808 -(void)testPCSUnencryptedFieldsRecieveAuthenticatedFields {
809 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
811 [self startCKKSSubsystem];
812 [self.keychainView waitForKeyHierarchyReadiness];
813 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
815 NSNumber* servIdentifier = @3;
816 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
817 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
819 NSError* error = nil;
821 // Manually encrypt an item
822 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
823 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
824 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
825 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
826 parentKeyUUID:self.keychainZoneKeys.classC.uuid
827 zoneID:recordID.zoneID];
828 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
829 XCTAssertNotNil(itemkey, "Got a key");
830 cipheritem.wrappedkey = itemkey.wrappedkey;
831 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
833 cipheritem.encver = CKKSItemEncryptionVersion2;
835 // This item has the PCS public fields, and they are authenticated (since we're using v2)
836 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
837 cipheritem.plaintextPCSPublicKey = publicKey;
838 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
840 // Use version 2, so PCS plaintext fields will be authenticated
841 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
843 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
844 XCTAssertNil(error, "no error encrypting object");
845 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
847 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
849 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
850 [self.keychainView waitForFetchAndIncomingQueueProcessing];
852 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
853 (id)kSecReturnAttributes: @YES,
854 (id)kSecAttrSynchronizable: @YES,
855 (id)kSecAttrAccount: @"account-delete-me",
856 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
858 CFTypeRef cfresult = NULL;
859 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
861 NSDictionary* result = CFBridgingRelease(cfresult);
862 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
863 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
864 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
866 // Test that if this item is updated, it remains encrypted in v2
867 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
868 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
869 PCSServiceIdentifier:(NSNumber *)servIdentifier
870 PCSPublicKey:publicKey
871 PCSPublicIdentity:publicIdentity]];
872 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
874 OCMVerifyAllWithDelay(self.mockDatabase, 20);
875 [self waitForCKModifications];
877 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
878 XCTAssertEqualObjects(newRecord[SecCKRecordPCSServiceIdentifier], servIdentifier, "Didn't change service identifier");
879 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicKey], publicKey, "Didn't change public key");
880 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicIdentity], publicIdentity, "Didn't change public identity");
881 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
884 -(void)testResetLocal {
885 // Test starts with nothing in database, but one in our fake CloudKit.
886 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
887 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
888 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
890 // Spin up CKKS subsystem.
891 [self startCKKSSubsystem];
893 // We expect a single record to be uploaded
894 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
895 [self addGenericPassword: @"data" account: @"account-delete-me"];
896 OCMVerifyAllWithDelay(self.mockDatabase, 20);
898 // After the local reset, we expect: a fetch, then nothing
899 self.silentFetchesAllowed = false;
900 [self expectCKFetch];
902 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"];
903 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
904 XCTAssertNil(result, "no error resetting local");
905 [resetExpectation fulfill];
907 [self waitForExpectations:@[resetExpectation] timeout:20];
909 OCMVerifyAllWithDelay(self.mockDatabase, 20);
911 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
912 [self addGenericPassword:@"asdf"
913 account:@"account-class-A"
915 access:(id)kSecAttrAccessibleWhenUnlocked
916 expecting:errSecSuccess
917 message:@"Adding class A item"];
918 OCMVerifyAllWithDelay(self.mockDatabase, 20);
921 -(void)testResetLocalWhileUntrusted {
922 // We're "logged in to" cloudkit but not in circle.
923 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
924 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
925 self.silentFetchesAllowed = false;
927 // Test starts with local TLK and key hierarchy in our fake cloudkit
928 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
929 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
931 // Spin up CKKS subsystem. It should fetch once.
932 [self expectCKFetch];
933 [self startCKKSSubsystem];
935 XCTAssertEqual(0, [self.keychainView.loggedIn wait:500*NSEC_PER_MSEC], "Should have been told of a 'login' event on startup");
937 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], @"Key state should arrive at 'waitfortrust''");
938 OCMVerifyAllWithDelay(self.mockDatabase, 20);
940 NSData* changeTokenData = [[[NSUUID UUID] UUIDString] dataUsingEncoding:NSUTF8StringEncoding];
941 CKServerChangeToken* changeToken = [[CKServerChangeToken alloc] initWithData:changeTokenData];
942 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
943 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
944 ckse.changeToken = changeToken;
946 NSError* error = nil;
947 [ckse saveToDatabase:&error];
948 XCTAssertNil(error, "No error saving new zone state to database");
949 return CKKSDatabaseTransactionCommit;
952 // after the reset, CKKS should refetch what's available
953 [self expectCKFetch];
955 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"];
956 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
957 XCTAssertNil(result, "no error resetting local");
958 ckksnotice_global("ckks", "Received a rpcResetLocal callback");
960 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
961 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
962 XCTAssertNotEqualObjects(changeToken, ckse.changeToken, "Change token is reset");
965 [resetExpectation fulfill];
968 [self waitForExpectations:@[resetExpectation] timeout:20];
970 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], @"Key state should arrive at 'waitfortrust''");
972 // Now regain trust, and see what happens! It should use the existing fetch, pick up the old key hierarchy, and use it
973 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
975 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
976 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
977 [self beginSOSTrustedViewOperation:self.keychainView];
979 OCMVerifyAllWithDelay(self.mockDatabase, 20);
980 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should arrive at 'ready''");
982 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
983 [self addGenericPassword:@"asdf"
984 account:@"account-class-A"
986 access:(id)kSecAttrAccessibleWhenUnlocked
987 expecting:errSecSuccess
988 message:@"Adding class A item"];
989 OCMVerifyAllWithDelay(self.mockDatabase, 20);
992 - (void)testResetLocalWhileLoggedOut {
993 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
994 self.accountStatus = CKAccountStatusNoAccount;
995 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
996 self.silentFetchesAllowed = false;
998 // Test starts with local TLK and key hierarchy in our fake cloudkit
999 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1000 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1002 [self startCKKSSubsystem];
1004 XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "CKKS should positively log out");
1005 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], @"Key state should arrive at 'loggedout'");
1007 // Can we reset local data while logged out?
1008 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"];
1009 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
1010 XCTAssertNil(result, "no error resetting local");
1011 ckksnotice_global("ckks", "Received a rpcResetLocal callback");
1013 [resetExpectation fulfill];
1016 [self waitForExpectations:@[resetExpectation] timeout:20];
1018 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], @"Key state should arrive at 'loggedout'");
1021 -(void)testResetLocalMultipleTimes {
1022 // Test starts with nothing in database, but one in our fake CloudKit.
1023 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1024 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1025 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1027 // Spin up CKKS subsystem.
1028 [self startCKKSSubsystem];
1030 // We expect a single record to be uploaded
1031 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1032 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1033 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1034 [self addGenericPassword: @"data" account: @"account-delete-me"];
1035 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1036 [self waitForCKModifications];
1038 // We're going to request a bunch of CloudKit resets, but hold them from finishing
1039 [self holdCloudKitFetches];
1041 XCTestExpectation* resetExpectation0 = [self expectationWithDescription: @"reset callback(0) occurs"];
1042 XCTestExpectation* resetExpectation1 = [self expectationWithDescription: @"reset callback(1) occurs"];
1043 XCTestExpectation* resetExpectation2 = [self expectationWithDescription: @"reset callback(2) occurs"];
1044 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
1045 XCTAssertNil(result, "should receive no error resetting local");
1046 secnotice("ckksreset", "Received a rpcResetLocal(0) callback");
1047 [resetExpectation0 fulfill];
1049 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
1050 XCTAssertNil(result, "should receive no error resetting local");
1051 secnotice("ckksreset", "Received a rpcResetLocal(1) callback");
1052 [resetExpectation1 fulfill];
1054 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
1055 XCTAssertNil(result, "should receive no error resetting local");
1056 secnotice("ckksreset", "Received a rpcResetLocal(2) callback");
1057 [resetExpectation2 fulfill];
1060 // After the reset(s), we expect no uploads. Let the resets flow!
1061 [self releaseCloudKitFetchHold];
1062 [self waitForExpectations:@[resetExpectation0, resetExpectation1, resetExpectation2] timeout:20];
1063 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1065 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1067 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1068 checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1069 [self addGenericPassword:@"asdf"
1070 account:@"account-class-A"
1072 access:(id)kSecAttrAccessibleWhenUnlocked
1073 expecting:errSecSuccess
1074 message:@"Adding class A item"];
1075 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1078 -(void)testResetCloudKitZone {
1079 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1080 OCMExpect([self.suggestTLKUpload trigger]);
1082 self.silentZoneDeletesAllowed = true;
1084 // Test starts with nothing in database, but one in our fake CloudKit.
1085 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1086 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1087 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1089 // Spin up CKKS subsystem.
1090 [self startCKKSSubsystem];
1092 // We expect a single record to be uploaded
1093 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1094 [self addGenericPassword: @"data" account: @"account-delete-me"];
1095 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1096 [self waitForCKModifications];
1097 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1099 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
1100 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1102 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1103 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1104 XCTAssertNil(result, "no error resetting cloudkit");
1105 ckksnotice_global("ckks", "Received a resetCloudKit callback");
1106 [resetExpectation fulfill];
1109 // Sneak in and perform Octagon's duties
1110 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1111 [self performOctagonTLKUpload:self.ckksViews];
1113 [self waitForExpectations:@[resetExpectation] timeout:20];
1115 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1117 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1118 [self addGenericPassword:@"asdf"
1119 account:@"account-class-A"
1121 access:(id)kSecAttrAccessibleWhenUnlocked
1122 expecting:errSecSuccess
1123 message:@"Adding class A item"];
1124 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1126 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1129 - (void)testResetCloudKitZoneCloudKitRejects {
1130 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1131 OCMExpect([self.suggestTLKUpload trigger]);
1133 self.nextModifyRecordZonesError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
1134 code:CKErrorZoneBusy
1136 CKErrorRetryAfterKey: @(0.2),
1137 NSUnderlyingErrorKey: [[CKPrettyError alloc] initWithDomain:CKErrorDomain
1141 self.silentZoneDeletesAllowed = true;
1143 // Test starts with nothing in database, but one in our fake CloudKit.
1144 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1145 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1146 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1148 // Spin up CKKS subsystem.
1149 [self startCKKSSubsystem];
1151 // We expect a single record to be uploaded
1152 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1153 [self addGenericPassword: @"data" account: @"account-delete-me"];
1154 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1155 [self waitForCKModifications];
1157 // During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
1158 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1160 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1161 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1162 XCTAssertNil(result, "no error resetting cloudkit");
1163 ckksnotice_global("ckks", "Received a resetCloudKit callback");
1164 [resetExpectation fulfill];
1167 // Sneak in and perform Octagon's duties
1168 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1169 [self performOctagonTLKUpload:self.ckksViews];
1171 [self waitForExpectations:@[resetExpectation] timeout:20];
1173 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1175 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1176 [self addGenericPassword:@"asdf"
1177 account:@"account-class-A"
1179 access:(id)kSecAttrAccessibleWhenUnlocked
1180 expecting:errSecSuccess
1181 message:@"Adding class A item"];
1182 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1184 XCTAssertNil(self.nextModifyRecordZonesError, "Record zone modification error should have been cleared");
1187 - (void)testResetCloudKitZoneDuringWaitForTLK {
1188 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1189 OCMExpect([self.suggestTLKUpload trigger]);
1191 self.silentZoneDeletesAllowed = true;
1193 // Test starts with nothing in database, but one in our fake CloudKit.
1195 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1196 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1198 // Spin up CKKS subsystem.
1199 [self startCKKSSubsystem];
1201 // No records should be uploaded
1202 [self addGenericPassword: @"data" account: @"account-delete-me"];
1204 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS should have entered waitfortlk");
1206 // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going)
1207 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1208 self.ckksViews = [NSMutableSet setWithObject:self.keychainView];
1210 [self beginSOSTrustedViewOperation:self.keychainView];
1211 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1213 CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil];
1214 XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold");
1216 // Now, reset everything. The outgoingOp should get cancelled.
1217 // We expect a key hierarchy upload, and then the class C item upload
1218 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1219 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1221 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1222 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1223 XCTAssertNil(result, "no error resetting cloudkit");
1224 [resetExpectation fulfill];
1227 // Sneak in and perform Octagon's duties
1228 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1229 [self performOctagonTLKUpload:self.ckksViews];
1231 [self waitForExpectations:@[resetExpectation] timeout:20];
1233 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1235 // And adding another item works too
1236 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1237 [self addGenericPassword:@"asdf"
1238 account:@"account-class-A"
1240 access:(id)kSecAttrAccessibleWhenUnlocked
1241 expecting:errSecSuccess
1242 message:@"Adding class A item"];
1243 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1245 XCTAssertTrue([outgoingOp isFinished], "old ProcessOutgoingQueue should be finished");
1249 * This test doesn't work, since the resetLocal fails. CKKS gets back into waitfortlk
1250 * but that isn't considered a successful resetLocal.
1252 - (void)testResetLocalDuringWaitForTLK {
1253 // Test starts with nothing in database, but one in our fake CloudKit.
1255 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1257 // Spin up CKKS subsystem.
1258 [self startCKKSSubsystem];
1260 // No records should be uploaded
1261 [self addGenericPassword: @"data" account: @"account-delete-me"];
1263 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS should have entered waitfortlk");
1265 // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going)
1266 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
1267 [self beginSOSTrustedViewOperation:self.keychainView];
1268 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1270 CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil];
1271 XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold");
1273 // Now, reset everything. The outgoingOp should get cancelled.
1274 // We expect a key hierarchy upload, and then the class C item upload
1275 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords:3 zoneID:self.keychainZoneID];
1276 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
1277 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1279 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1280 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
1281 XCTAssertNil(result, "no error resetting local");
1282 [resetExpectation fulfill];
1284 [self waitForExpectations:@[resetExpectation] timeout:20];
1286 XCTAssertTrue([outgoingOp isCancelled], "old stuck ProcessOutgoingQueue should be cancelled");
1287 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1289 // And adding another item works too
1290 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1291 [self addGenericPassword:@"asdf"
1292 account:@"account-class-A"
1294 access:(id)kSecAttrAccessibleWhenUnlocked
1295 expecting:errSecSuccess
1296 message:@"Adding class A item"];
1297 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1300 -(void)testResetCloudKitZoneWhileUntrusted {
1301 self.silentZoneDeletesAllowed = true;
1303 // We're "logged in to" cloudkit but not in circle.
1304 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1305 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1307 // Test starts with nothing in database, but one in our fake CloudKit.
1308 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1310 // Spin up CKKS subsystem.
1311 [self startCKKSSubsystem];
1313 // Since CKKS is untrusted, it'll fetch the zone but then get stuck
1314 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortrust'");
1316 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
1317 [self.keychainZone addToZone: ckr];
1319 XCTAssertNotNil(self.keychainZone.currentDatabase, "Zone exists");
1320 XCTAssertNotNil(self.keychainZone.currentDatabase[ckr.recordID], "An item exists in the fake zone");
1322 // Make the zone, so we know if it was deleted
1323 self.keychainZone.flag = true;
1325 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1326 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1327 XCTAssertNil(result, "no error resetting cloudkit");
1328 ckksnotice_global("ckks", "Received a resetCloudKit callback");
1329 [resetExpectation fulfill];
1332 [self waitForExpectations:@[resetExpectation] timeout:20];
1334 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], "CKKS entered 'waitfortlkcreation'");
1336 XCTAssertFalse(self.keychainZone.flag, "Zone was deleted at some point");
1337 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1339 // Now log in, and see what happens! It should create the zone again and upload a whole new key hierarchy
1340 self.silentFetchesAllowed = true;
1341 self.mockSOSAdapter.circleStatus = kSOSCCInCircle;
1342 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1343 [self beginSOSTrustedViewOperation:self.keychainView];
1345 [self performOctagonTLKUpload:self.ckksViews];
1347 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1349 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1350 [self addGenericPassword:@"asdf"
1351 account:@"account-class-A"
1353 access:(id)kSecAttrAccessibleWhenUnlocked
1354 expecting:errSecSuccess
1355 message:@"Adding class A item"];
1356 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1359 - (void)testResetCloudKitZoneMultipleTimes {
1360 self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
1361 OCMExpect([self.suggestTLKUpload trigger]);
1363 self.silentZoneDeletesAllowed = true;
1365 // Test starts with nothing in database, but one in our fake CloudKit.
1366 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1367 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1368 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1370 // Spin up CKKS subsystem.
1371 [self startCKKSSubsystem];
1373 // We expect a single record to be uploaded
1374 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1375 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1376 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1377 [self addGenericPassword: @"data" account: @"account-delete-me"];
1378 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1379 [self waitForCKModifications];
1381 // We're going to request a bunch of CloudKit resets, but hold them from finishing
1382 [self holdCloudKitFetches];
1384 XCTestExpectation* resetExpectation0 = [self expectationWithDescription: @"reset callback(0) occurs"];
1385 XCTestExpectation* resetExpectation1 = [self expectationWithDescription: @"reset callback(1) occurs"];
1386 XCTestExpectation* resetExpectation2 = [self expectationWithDescription: @"reset callback(2) occurs"];
1387 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1388 XCTAssertNil(result, "should receive no error resetting cloudkit");
1389 secnotice("ckksreset", "Received a resetCloudKit(0) callback");
1390 [resetExpectation0 fulfill];
1392 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1393 XCTAssertNil(result, "should receive no error resetting cloudkit");
1394 secnotice("ckksreset", "Received a resetCloudKit(1) callback");
1395 [resetExpectation1 fulfill];
1397 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1398 XCTAssertNil(result, "should receive no error resetting cloudkit");
1399 secnotice("ckksreset", "Received a resetCloudKit(2) callback");
1400 [resetExpectation2 fulfill];
1403 // After the reset(s), we expect a key hierarchy upload, and then the class C item upload
1404 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1405 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1407 // And let the resets flow
1408 [self releaseCloudKitFetchHold];
1410 OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
1411 [self performOctagonTLKUpload:self.ckksViews];
1413 [self waitForExpectations:@[resetExpectation0, resetExpectation1, resetExpectation2] timeout:20];
1414 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1416 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1418 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1419 checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1420 [self addGenericPassword:@"asdf"
1421 account:@"account-class-A"
1423 access:(id)kSecAttrAccessibleWhenUnlocked
1424 expecting:errSecSuccess
1425 message:@"Adding class A item"];
1426 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1429 - (void)testRPCFetchAndProcessWhileCloudKitNotResponding {
1430 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1431 [self startCKKSSubsystem];
1433 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1434 [self holdCloudKitFetches];
1436 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1437 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1438 // done! we should have an underlying error of "fetch isn't working"
1439 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1440 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1441 XCTAssertNotNil(underlying, "Should have received an underlying error");
1442 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1443 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingSuccessfulFetch, "Underlying error should be 'pending fetch'");
1444 [callbackOccurs fulfill];
1447 [self waitForExpectations:@[callbackOccurs] timeout:20];
1448 [self releaseCloudKitFetchHold];
1449 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1452 - (void)testRPCFetchAndProcessWhileCloudKitErroring {
1453 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1454 [self startCKKSSubsystem];
1456 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1458 [self.keychainZone failNextFetchWith:[[CKPrettyError alloc] initWithDomain:CKErrorDomain
1459 code:CKErrorRequestRateLimited
1460 userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:30]}]];
1462 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1463 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1464 // done! we should have an underlying error of "fetch isn't working"
1465 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1466 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1467 XCTAssertNotNil(underlying, "Should have received an underlying error");
1468 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1469 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingSuccessfulFetch, "Underlying error should be 'pending fetch'");
1471 NSError* underunderlying = underlying.userInfo[NSUnderlyingErrorKey];
1472 XCTAssertNotNil(underunderlying, "Should have received another layer of underlying error");
1473 XCTAssertEqualObjects(underunderlying.domain, CKErrorDomain, "Underlying error should be CKErrorDomain");
1474 XCTAssertEqual(underunderlying.code, CKErrorRequestRateLimited, "Underlying error should be 'rate limited'");
1476 [callbackOccurs fulfill];
1479 [self waitForExpectations:@[callbackOccurs] timeout:20];
1480 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1483 - (void)testRPCFetchAndProcessWhileInWaitForTLK {
1484 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1485 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1486 [self startCKKSSubsystem];
1488 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1490 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1491 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1492 // done! we should have an underlying error of "fetch isn't working"
1493 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1494 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1495 XCTAssertNotNil(underlying, "Should have received an underlying error");
1496 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1497 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingKeyReady, "Underlying error should be 'pending key ready'");
1498 [callbackOccurs fulfill];
1501 [self waitForExpectations:@[callbackOccurs] timeout:20];
1502 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1505 - (void)testRPCTLKMissingWhenMissing {
1506 // Bring CKKS up in waitfortlk
1507 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1508 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1509 [self startCKKSSubsystem];
1511 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1513 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1515 [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) {
1516 XCTAssertTrue(missing, "TLKs should be missing");
1517 [callbackOccurs fulfill];
1520 [self waitForExpectations:@[callbackOccurs] timeout:20];
1522 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1525 - (void)testRPCTLKMissingWhenFound {
1526 // Bring CKKS up in 'ready'
1527 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1528 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1529 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1530 [self startCKKSSubsystem];
1532 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready''");
1534 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1536 [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) {
1537 XCTAssertFalse(missing, "TLKs should not be missing");
1538 [callbackOccurs fulfill];
1541 [self waitForExpectations:@[callbackOccurs] timeout:20];
1543 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1546 - (void)testRPCKnownBadStateWhenNoCloudKit {
1547 self.accountStatus = CKAccountStatusNoAccount;
1549 [self startCKKSSubsystem];
1551 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:20*NSEC_PER_SEC], "CKKS entered loggedout");
1553 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1555 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1556 XCTAssertEqual(result, CKKSKnownStateNoCloudKitAccount, "State should be 'no cloudkit account'");
1557 [callbackOccurs fulfill];
1560 [self waitForExpectations:@[callbackOccurs] timeout:20];
1562 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1565 - (void)testRPCKnownBadStateWhenNoTrust {
1566 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1567 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1569 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
1570 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1572 [self startCKKSSubsystem];
1574 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered waitfortrust");
1576 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1578 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1579 XCTAssertEqual(result, CKKSKnownStateWaitForOctagon, "State should be 'waitforoctagon'");
1580 [callbackOccurs fulfill];
1583 [self waitForExpectations:@[callbackOccurs] timeout:20];
1585 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1588 - (void)testRPCKnownBadStateWhenTLKsMissing {
1589 // Bring CKKS up in waitfortlk
1590 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1591 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1592 [self startCKKSSubsystem];
1594 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1596 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1598 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1599 XCTAssertEqual(result, CKKSKnownStateTLKsMissing, "TLKs should be missing");
1600 [callbackOccurs fulfill];
1603 [self waitForExpectations:@[callbackOccurs] timeout:20];
1605 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1608 - (void)testRPCKnownBadStateWhenInWaitForUnlock {
1609 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1610 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1611 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1613 // Bring CKKS up in 'waitforunlock'
1614 self.aksLockState = true;
1615 [self.lockStateTracker recheck];
1616 [self startCKKSSubsystem];
1618 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1619 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitforunlock");
1621 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1623 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1624 XCTAssertEqual(result, CKKSKnownStateWaitForUnlock, "known state should be wait for unlock");
1625 [callbackOccurs fulfill];
1628 [self waitForExpectations:@[callbackOccurs] timeout:20];
1630 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1633 - (void)testRPCKnownBadStateWhenInWaitForUpload {
1634 // Bring CKKS up in 'waitfortupload'
1635 self.aksLockState = true;
1636 [self.lockStateTracker recheck];
1637 [self startCKKSSubsystem];
1639 // Wait for the key hierarchy state machine to get stuck waiting for Octagon. No uploads should occur.
1640 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLKCreation] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitfortlkcreation");
1642 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1644 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1645 XCTAssertEqual(result, CKKSKnownStateWaitForOctagon, "known state should be wait for Octagon");
1646 [callbackOccurs fulfill];
1649 [self waitForExpectations:@[callbackOccurs] timeout:20];
1651 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1654 - (void)testRPCKnownBadStateWhenInGoodState {
1655 // Bring CKKS up in 'ready'
1656 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1657 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1658 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1659 [self startCKKSSubsystem];
1661 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready''");
1663 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1665 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1666 XCTAssertEqual(result, CKKSKnownStatePossiblyGood, "known state should not be possibly-good");
1667 [callbackOccurs fulfill];
1670 [self waitForExpectations:@[callbackOccurs] timeout:20];
1672 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1675 - (void)testRpcStatus {
1676 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1678 [self startCKKSSubsystem];
1680 // Let things shake themselves out.
1681 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1682 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should return to 'ready'");
1683 [self waitForCKModifications];
1685 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1686 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1687 XCTAssertNil(error, "should be no error fetching status for keychain");
1689 // Ugly "global" hack
1690 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1691 NSDictionary* keychainStatus = result[1];
1693 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1694 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1695 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status");
1696 XCTAssertNotNil(keychainStatus[@"ckmirror"], "Status should have any ckmirror");
1697 [callbackOccurs fulfill];
1700 [self waitForExpectations:@[callbackOccurs] timeout:20];
1703 - (void)testRpcFastStatus {
1704 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1706 [self startCKKSSubsystem];
1708 // Let things shake themselves out.
1709 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1710 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should return to 'ready'");
1711 [self waitForCKModifications];
1713 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1714 [self.ckksControl rpcFastStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1715 XCTAssertNil(error, "should be no error fetching status for keychain");
1717 // Ugly "global" hack
1718 XCTAssertEqual(result.count, 1u, "Should have received one result dictionaries back");
1719 NSDictionary* keychainStatus = result[0];
1721 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1722 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1723 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status");
1724 XCTAssertNil(keychainStatus[@"ckmirror"], "fastStatus should not have any ckmirror");
1725 [callbackOccurs fulfill];
1728 [self waitForExpectations:@[callbackOccurs] timeout:20];
1732 - (void)testRpcStatusWaitsForAccountDetermination {
1733 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1735 // Set up the account state callbacks to happen in one second
1736 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (1 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
1737 // Let CKKS come up (simulating daemon starting due to RPC)
1738 [self startCKKSSubsystem];
1741 // Before CKKS figures out we're in an account, fire off the status RPC.
1742 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1743 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1744 XCTAssertNil(error, "should be no error fetching status for keychain");
1746 // Ugly "global" hack
1747 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1748 NSDictionary* keychainStatus = result[1];
1750 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1751 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1752 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status");
1753 [callbackOccurs fulfill];
1756 [self waitForExpectations:@[callbackOccurs] timeout:20];
1759 - (void)testRpcStatusIsFastDuringError {
1760 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1762 // Let CKKS come up, then force it into error
1763 [self startCKKSSubsystem];
1764 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1766 OctagonStateTransitionOperation* op = [OctagonStateTransitionOperation named:@"enter" entering:SecCKKSZoneKeyStateError];
1767 OctagonStateTransitionRequest* request = [[OctagonStateTransitionRequest alloc] init:@"enter-wait-for-trust"
1768 sourceStates:[NSSet setWithArray:[CKKSZoneKeyStateMap() allKeys]]
1769 serialQueue:self.keychainView.queue
1770 timeout:10 * NSEC_PER_SEC
1772 [self.keychainView.stateMachine handleExternalRequest:request];
1774 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateError] wait:20*NSEC_PER_SEC], "CKKS entered 'error'");
1776 // Fire off the status RPC; it should return immediately
1777 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1778 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1779 XCTAssertNil(error, "should be no error fetching status for keychain");
1781 // Ugly "global" hack
1782 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1783 NSDictionary* keychainStatus = result[1];
1785 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1786 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1787 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateError, "Should be in 'ready' status");
1788 [callbackOccurs fulfill];
1791 [self waitForExpectations:@[callbackOccurs] timeout:20];
1794 - (void)testResetLocalAPIWakesDaemon {
1795 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
1796 [self startCKKSSubsystem];
1798 // We expect a single record to be uploaded
1799 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1800 [self addGenericPassword:@"data" account:@"account-delete-me"];
1801 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1802 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1804 // Now, simulate a restart: all views suddenly go missing from the ViewManager
1805 [self.injectedManager clearAllViews];
1806 [self.injectedManager resetSyncingPolicy];
1807 [self.injectedManager beginCloudKitOperationOfAllViews];
1809 // And a reset-local API call wakes the daemon
1810 // The policy arrives after 500ms
1811 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (0.5 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
1812 [self.injectedManager setCurrentSyncingPolicy:self.viewSortingPolicyForManagedViewList];
1813 self.ckksViews = [NSMutableSet setWithArray:[self.injectedManager.views allValues]];
1814 self.keychainView = [self.injectedManager findView:@"keychain"];
1815 [self beginSOSTrustedOperationForAllViews];
1818 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"];
1819 [self.injectedManager rpcResetLocal:self.keychainZoneID.zoneName reply:^(NSError* result) {
1820 XCTAssertNil(result, "no error resetting local");
1821 [resetExpectation fulfill];
1824 [self waitForExpectations:@[resetExpectation] timeout:20];
1826 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1827 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1830 - (void)testPushAPIWakesDaemon {
1831 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
1832 [self startCKKSSubsystem];
1834 // We expect a single record to be uploaded
1835 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1836 [self addGenericPassword:@"data" account:@"account-delete-me"];
1837 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1838 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1840 // Now, simulate a restart: all views suddenly go missing from the ViewManager
1841 [self.injectedManager clearAllViews];
1842 [self.injectedManager resetSyncingPolicy];
1843 [self.injectedManager beginCloudKitOperationOfAllViews];
1845 // And a fetch API call wakes the daemon
1846 // The policy arrives after 500msx
1847 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (0.5 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
1848 [self.injectedManager setCurrentSyncingPolicy:self.viewSortingPolicyForManagedViewList];
1849 self.ckksViews = [NSMutableSet setWithArray:[self.injectedManager.views allValues]];
1850 [self beginSOSTrustedOperationForAllViews];
1853 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1854 [self.ckksControl rpcPushOutgoingChanges:nil reply:^(NSError * _Nullable error) {
1855 XCTAssertNil(error, "Should have received no error");
1856 [callbackOccurs fulfill];
1859 [self waitForExpectations:@[callbackOccurs] timeout:60];
1860 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1863 - (void)testFetchAPIWakesDaemon {
1864 [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
1865 [self startCKKSSubsystem];
1867 // We expect a single record to be uploaded
1868 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1869 [self addGenericPassword:@"data" account:@"account-delete-me"];
1870 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1871 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1873 // Now, simulate a restart: all views suddenly go missing from the ViewManager
1874 [self.injectedManager clearAllViews];
1875 [self.injectedManager resetSyncingPolicy];
1876 [self.injectedManager beginCloudKitOperationOfAllViews];
1878 // And a fetch API call wakes the daemon
1879 // The policy arrives after 500ms
1880 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (0.5 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
1881 [self.injectedManager setCurrentSyncingPolicy:self.viewSortingPolicyForManagedViewList];
1882 self.ckksViews = [NSMutableSet setWithArray:[self.injectedManager.views allValues]];
1883 [self beginSOSTrustedOperationForAllViews];
1886 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1887 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1888 XCTAssertNil(error, "Should have received no error");
1889 [callbackOccurs fulfill];
1892 [self waitForExpectations:@[callbackOccurs] timeout:20];
1893 OCMVerifyAllWithDelay(self.mockDatabase, 20);