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 <Security/SecEntitlements.h>
32 #include <ipc/server_security_helpers.h>
33 #import <Foundation/NSXPCConnection_Private.h>
35 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
36 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
37 #import "keychain/ckks/CKKS.h"
38 #import "keychain/ckks/CKKSItem.h"
39 #import "keychain/ckks/CKKSItemEncrypter.h"
40 #import "keychain/ckks/CKKSKey.h"
41 #import "keychain/ckks/CKKSViewManager.h"
42 #import "keychain/ckks/CKKSZoneStateEntry.h"
44 #import "keychain/ckks/CKKSControl.h"
45 #import "keychain/ckks/CloudKitCategories.h"
47 #import "keychain/ckks/tests/MockCloudKit.h"
48 #import "keychain/ckks/tests/CKKSTests.h"
49 #import "keychain/ckks/tests/CKKSTests+API.h"
51 @implementation CloudKitKeychainSyncingTestsBase (APITests)
53 -(NSMutableDictionary*)pcsAddItemQuery:(NSString*)account
55 serviceIdentifier:(NSNumber*)serviceIdentifier
56 publicKey:(NSData*)publicKey
57 publicIdentity:(NSData*)publicIdentity
60 (id)kSecClass : (id)kSecClassGenericPassword,
61 (id)kSecReturnPersistentRef: @YES,
62 (id)kSecReturnAttributes: @YES,
63 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
64 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
65 (id)kSecAttrAccount : account,
66 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
67 (id)kSecValueData : data,
68 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
69 (id)kSecAttrPCSPlaintextServiceIdentifier : serviceIdentifier,
70 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
71 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
75 -(NSDictionary*)pcsAddItem:(NSString*)account
77 serviceIdentifier:(NSNumber*)serviceIdentifier
78 publicKey:(NSData*)publicKey
79 publicIdentity:(NSData*)publicIdentity
80 expectingSync:(bool)expectingSync
82 NSMutableDictionary* query = [self pcsAddItemQuery:account
84 serviceIdentifier:(NSNumber*)serviceIdentifier
85 publicKey:(NSData*)publicKey
86 publicIdentity:(NSData*)publicIdentity];
87 CFTypeRef result = NULL;
88 XCTestExpectation* syncExpectation = [self expectationWithDescription: @"callback occurs"];
90 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, &result, ^(bool didSync, CFErrorRef error) {
92 XCTAssertTrue(didSync, "Item synced");
93 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
95 XCTAssertFalse(didSync, "Item did not sync");
96 XCTAssertNotNil((__bridge NSError*)error, "Error syncing item");
99 [syncExpectation fulfill];
100 }), @"_SecItemAddAndNotifyOnSync succeeded");
102 // Verify that the item was written to CloudKit
103 OCMVerifyAllWithDelay(self.mockDatabase, 8);
105 // In real code, you'd need to wait for the _SecItemAddAndNotifyOnSync callback to succeed before proceeding
106 [self waitForExpectations:@[syncExpectation] timeout:8.0];
108 return (NSDictionary*) CFBridgingRelease(result);
111 - (BOOL (^) (CKRecord*)) checkPCSFieldsBlock: (CKRecordZoneID*) zoneID
112 PCSServiceIdentifier:(NSNumber*)servIdentifier
113 PCSPublicKey:(NSData*)publicKey
114 PCSPublicIdentity:(NSData*)publicIdentity
116 __weak __typeof(self) weakSelf = self;
117 return ^BOOL(CKRecord* record) {
118 __strong __typeof(weakSelf) strongSelf = weakSelf;
119 XCTAssertNotNil(strongSelf, "self exists");
121 XCTAssert([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier], "PCS Service identifier matches input");
122 XCTAssert([record[SecCKRecordPCSPublicKey] isEqual: publicKey], "PCS Public Key matches input");
123 XCTAssert([record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity], "PCS Public Identity matches input");
125 if([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier] &&
126 [record[SecCKRecordPCSPublicKey] isEqual: publicKey] &&
127 [record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity]) {
136 @interface CloudKitKeychainSyncingAPITests : CloudKitKeychainSyncingTestsBase
139 @implementation CloudKitKeychainSyncingAPITests
140 - (void)testSecuritydClientBringup {
142 CFErrorRef cferror = nil;
143 xpc_endpoint_t endpoint = SecCreateSecuritydXPCServerEndpoint(&cferror);
144 XCTAssertNil((__bridge id)cferror, "No error creating securityd endpoint");
145 XCTAssertNotNil(endpoint, "Received securityd endpoint");
148 NSXPCInterface *interface = [NSXPCInterface interfaceWithProtocol:@protocol(SecuritydXPCProtocol)];
149 [SecuritydXPCClient configureSecuritydXPCProtocol: interface];
150 XCTAssertNotNil(interface, "Received a configured CKKS interface");
153 NSXPCListenerEndpoint *listenerEndpoint = [[NSXPCListenerEndpoint alloc] init];
154 [listenerEndpoint _setEndpoint:endpoint];
156 NSXPCConnection* connection = [[NSXPCConnection alloc] initWithListenerEndpoint:listenerEndpoint];
157 XCTAssertNotNil(connection , "Received an active connection");
159 connection.remoteObjectInterface = interface;
163 - (void)testAddAndNotifyOnSync {
164 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
166 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
167 [self startCKKSSubsystem];
169 // Let things shake themselves out.
170 [self.keychainView waitForKeyHierarchyReadiness];
171 [self waitForCKModifications];
173 NSMutableDictionary* query = [@{
174 (id)kSecClass : (id)kSecClassGenericPassword,
175 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
176 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
177 (id)kSecAttrAccount : @"testaccount",
178 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
179 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
182 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
184 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
185 XCTAssertTrue(didSync, "Item synced properly");
186 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
188 [blockExpectation fulfill];
189 }), @"_SecItemAddAndNotifyOnSync succeeded");
191 [self waitForExpectationsWithTimeout:5.0 handler:nil];
194 - (void)testAddAndNotifyOnSyncFailure {
195 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
197 [self startCKKSSubsystem];
198 [self.keychainView waitForFetchAndIncomingQueueProcessing];
200 // Due to item UUID selection, this item will be added with UUID 50184A35-4480-E8BA-769B-567CF72F1EC0.
201 // Add it to CloudKit first!
202 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"50184A35-4480-E8BA-769B-567CF72F1EC0"];
203 [self.keychainZone addToZone: ckr];
207 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
209 NSMutableDictionary* query = [@{
210 (id)kSecClass : (id)kSecClassGenericPassword,
211 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
212 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
213 (id)kSecAttrAccount : @"testaccount",
214 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
215 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
216 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // @ fake view hint for fake view
219 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
221 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
222 XCTAssertFalse(didSync, "Item did not sync (as expected)");
223 XCTAssertNotNil((__bridge NSError*)error, "error exists when item fails to sync");
225 [blockExpectation fulfill];
226 }), @"_SecItemAddAndNotifyOnSync succeeded");
228 [self waitForExpectationsWithTimeout:5.0 handler:nil];
229 [self waitForCKModifications];
232 - (void)testAddAndNotifyOnSyncLoggedOut {
233 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
234 self.accountStatus = CKAccountStatusNoAccount;
235 self.silentFetchesAllowed = false;
236 [self startCKKSSubsystem];
238 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2*NSEC_PER_SEC], "CKKS should positively log out");
240 NSMutableDictionary* query = [@{
241 (id)kSecClass : (id)kSecClassGenericPassword,
242 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
243 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
244 (id)kSecAttrAccount : @"testaccount",
245 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
246 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
249 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
251 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
252 XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)");
253 XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out");
255 [blockExpectation fulfill];
256 }), @"_SecItemAddAndNotifyOnSync succeeded");
258 [self waitForExpectationsWithTimeout:5.0 handler:nil];
261 - (void)testAddAndNotifyOnSyncAccountStatusUnclear {
262 // Test starts with nothing in database, but CKKS hasn't been told we've logged out yet.
263 // We expect no CKKS operations.
264 self.accountStatus = CKAccountStatusNoAccount;
265 self.silentFetchesAllowed = false;
267 NSMutableDictionary* query = [@{
268 (id)kSecClass : (id)kSecClassGenericPassword,
269 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
270 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
271 (id)kSecAttrAccount : @"testaccount",
272 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
273 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
276 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
278 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
279 XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)");
280 XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out");
282 [blockExpectation fulfill];
283 }), @"_SecItemAddAndNotifyOnSync succeeded");
285 // And now, allow CKKS to discover we're logged out
286 [self startCKKSSubsystem];
287 XCTAssertEqual(0, [self.keychainView.loggedOut wait:2*NSEC_PER_SEC], "CKKS should positively log out");
289 [self waitForExpectationsWithTimeout:5.0 handler:nil];
292 - (void)testAddAndNotifyOnSyncBeforeKeyHierarchyReady {
293 // Test starts with a key hierarchy in cloudkit and the TLK having arrived
294 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
295 [self saveTLKMaterialToKeychain:self.keychainZoneID];
296 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
298 // But block CloudKit fetches (so the key hierarchy won't be ready when we add this new item)
299 [self holdCloudKitFetches];
301 [self startCKKSSubsystem];
302 XCTAssertEqual(0, [self.keychainView.loggedIn wait:2*NSEC_PER_SEC], "CKKS should log in");
303 [self.keychainView.zoneSetupOperation waitUntilFinished];
305 NSMutableDictionary* query = [@{
306 (id)kSecClass : (id)kSecClassGenericPassword,
307 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
308 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
309 (id)kSecAttrAccount : @"testaccount",
310 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
311 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
312 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
315 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
317 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
318 XCTAssertTrue(didSync, "Item synced");
319 XCTAssertNil((__bridge NSError*)error, "Shouldn't have received an error syncing item");
321 [blockExpectation fulfill];
322 }), @"_SecItemAddAndNotifyOnSync succeeded");
324 // We should be in the 'fetch' state, but no further
325 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateFetch] wait:100*NSEC_PER_MSEC], @"Should have reached key state 'fetch', but no further");
327 // When we release the fetch, the callback should still fire and the item should upload
328 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
329 [self releaseCloudKitFetchHold];
330 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:5*NSEC_PER_SEC], @"Should have reached key state 'ready'");
332 // Verify that the item was written to CloudKit
333 OCMVerifyAllWithDelay(self.mockDatabase, 8);
335 [self waitForExpectationsWithTimeout:5.0 handler:nil];
338 - (void)testPCSUnencryptedFieldsAdd {
339 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
341 [self startCKKSSubsystem];
342 [self.keychainView waitForKeyHierarchyReadiness];
344 NSNumber* servIdentifier = @3;
345 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
346 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
348 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
349 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
350 PCSServiceIdentifier:(NSNumber *)servIdentifier
351 PCSPublicKey:publicKey
352 PCSPublicIdentity:publicIdentity]];
354 NSMutableDictionary* query = [@{
355 (id)kSecClass : (id)kSecClassGenericPassword,
356 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
357 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
358 (id)kSecAttrAccount : @"testaccount",
359 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
360 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
361 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
362 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
363 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
364 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
365 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // allows a CKKSScanOperation to find this item
368 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
370 // Verify that the item is written to CloudKit
371 OCMVerifyAllWithDelay(self.mockDatabase, 4);
373 CFTypeRef item = NULL;
374 query[(id)kSecValueData] = nil;
375 query[(id)kSecReturnAttributes] = @YES;
376 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
378 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
379 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Service Identifier exists");
380 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key exists");
381 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity exists");
383 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
384 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
385 [self waitForCKModifications];
386 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
387 CKRecord* record = self.keychainZone.currentDatabase[recordID];
388 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
390 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], servIdentifier, "Service identifier sent to cloudkit");
391 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], publicKey, "public key sent to cloudkit");
392 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], publicIdentity, "public identity sent to cloudkit");
395 - (void)testPCSUnencryptedFieldsModify {
396 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
398 [self startCKKSSubsystem];
399 [self.keychainView waitForKeyHierarchyReadiness];
401 NSNumber* servIdentifier = @3;
402 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
403 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
405 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
406 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
407 PCSServiceIdentifier:(NSNumber *)servIdentifier
408 PCSPublicKey:publicKey
409 PCSPublicIdentity:publicIdentity]];
411 NSMutableDictionary* query = [@{
412 (id)kSecClass : (id)kSecClassGenericPassword,
413 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
414 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
415 (id)kSecAttrAccount : @"testaccount",
416 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
417 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
418 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
419 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
420 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
421 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
422 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // allows a CKKSScanOperation to find this item
425 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
427 OCMVerifyAllWithDelay(self.mockDatabase, 8);
428 [self waitForCKModifications];
430 query[(id)kSecValueData] = nil;
431 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
432 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
433 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
436 publicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
438 NSNumber* newServiceIdentifier = @10;
439 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
440 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
442 NSDictionary* update = @{
443 (id)kSecAttrPCSPlaintextServiceIdentifier : newServiceIdentifier,
444 (id)kSecAttrPCSPlaintextPublicKey : newPublicKey,
445 (id)kSecAttrPCSPlaintextPublicIdentity : newPublicIdentity,
448 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
449 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
450 PCSServiceIdentifier:(NSNumber *)newServiceIdentifier
451 PCSPublicKey:newPublicKey
452 PCSPublicIdentity:newPublicIdentity]];
454 XCTAssertEqual(errSecSuccess, SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update), @"SecItemUpdate succeeded");
455 OCMVerifyAllWithDelay(self.mockDatabase, 8);
457 CFTypeRef item = NULL;
458 query[(id)kSecValueData] = nil;
459 query[(id)kSecReturnAttributes] = @YES;
460 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
462 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
463 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], newServiceIdentifier, "Service Identifier exists");
464 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], newPublicKey, "public key exists");
465 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], newPublicIdentity, "public identity exists");
467 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
468 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
469 [self waitForCKModifications];
470 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
471 CKRecord* record = self.keychainZone.currentDatabase[recordID];
472 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
474 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], newServiceIdentifier, "Service identifier sent to cloudkit");
475 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], newPublicKey, "public key sent to cloudkit");
476 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], newPublicIdentity, "public identity sent to cloudkit");
479 // As of [<rdar://problem/32558310> CKKS: Re-authenticate PCSPublicFields], these fields are NOT server-modifiable. This test proves it.
480 - (void)testPCSUnencryptedFieldsServerModifyFail {
481 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
483 [self startCKKSSubsystem];
484 [self.keychainView waitForKeyHierarchyReadiness];
486 NSNumber* servIdentifier = @3;
487 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
488 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
490 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
491 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
492 PCSServiceIdentifier:(NSNumber *)servIdentifier
493 PCSPublicKey:publicKey
494 PCSPublicIdentity:publicIdentity]];
496 NSMutableDictionary* query = [@{
497 (id)kSecClass : (id)kSecClassGenericPassword,
498 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
499 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
500 (id)kSecAttrAccount : @"testaccount",
501 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
502 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
503 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
504 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
505 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
506 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
507 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // fake, for CKKSScanOperation
510 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
512 OCMVerifyAllWithDelay(self.mockDatabase, 4);
513 [self waitForCKModifications];
515 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
516 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
517 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
518 CKRecord* record = self.keychainZone.currentDatabase[recordID];
519 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
521 // Items are encrypted using encv2
522 XCTAssertEqualObjects(record[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
525 // Test has already failed; find the record just to be nice.
526 for(CKRecord* maybe in self.keychainZone.currentDatabase.allValues) {
527 if(maybe[SecCKRecordPCSServiceIdentifier] != nil) {
533 NSNumber* newServiceIdentifier = @10;
534 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
535 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
537 // Change the public key and public identity
538 record = [record copyWithZone: nil];
539 record[SecCKRecordPCSServiceIdentifier] = newServiceIdentifier;
540 record[SecCKRecordPCSPublicKey] = newPublicKey;
541 record[SecCKRecordPCSPublicIdentity] = newPublicIdentity;
542 [self.keychainZone addToZone: record];
544 // Trigger a notification
545 [self.keychainView notifyZoneChange:nil];
546 [self.keychainView waitForFetchAndIncomingQueueProcessing];
548 CFTypeRef item = NULL;
549 query[(id)kSecValueData] = nil;
550 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
551 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
552 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
553 query[(id)kSecReturnAttributes] = @YES;
554 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
556 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
557 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "service identifier is not updated");
558 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key not updated");
559 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity not updated");
562 -(void)testPCSUnencryptedFieldsRecieveUnauthenticatedFields {
563 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
565 [self startCKKSSubsystem];
566 [self.keychainView waitForKeyHierarchyReadiness];
568 NSNumber* servIdentifier = @3;
569 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
570 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
572 NSError* error = nil;
574 // Manually encrypt an item
575 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
576 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
577 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
578 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
579 parentKeyUUID:self.keychainZoneKeys.classC.uuid
580 zoneID:recordID.zoneID];
581 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
582 XCTAssertNotNil(itemkey, "Got a key");
583 cipheritem.wrappedkey = itemkey.wrappedkey;
584 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
586 cipheritem.encver = CKKSItemEncryptionVersion1;
588 // This item has the PCS public fields, but they are not authenticated
589 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
590 cipheritem.plaintextPCSPublicKey = publicKey;
591 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
593 NSDictionary<NSString*, NSData*>* authenticatedData = [cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion1];
594 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
595 XCTAssertNil(error, "no error encrypting object");
596 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
598 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
600 [self.keychainView waitForFetchAndIncomingQueueProcessing];
602 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
603 (id)kSecReturnAttributes: @YES,
604 (id)kSecAttrSynchronizable: @YES,
605 (id)kSecAttrAccount: @"account-delete-me",
606 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
608 CFTypeRef cfresult = NULL;
609 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
611 NSDictionary* result = CFBridgingRelease(cfresult);
612 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
613 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
614 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
617 -(void)testPCSUnencryptedFieldsRecieveAuthenticatedFields {
618 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
620 [self startCKKSSubsystem];
621 [self.keychainView waitForKeyHierarchyReadiness];
622 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
624 NSNumber* servIdentifier = @3;
625 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
626 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
628 NSError* error = nil;
630 // Manually encrypt an item
631 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
632 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
633 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
634 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
635 parentKeyUUID:self.keychainZoneKeys.classC.uuid
636 zoneID:recordID.zoneID];
637 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
638 XCTAssertNotNil(itemkey, "Got a key");
639 cipheritem.wrappedkey = itemkey.wrappedkey;
640 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
642 cipheritem.encver = CKKSItemEncryptionVersion2;
644 // This item has the PCS public fields, and they are authenticated (since we're using v2)
645 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
646 cipheritem.plaintextPCSPublicKey = publicKey;
647 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
649 // Use version 2, so PCS plaintext fields will be authenticated
650 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
652 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
653 XCTAssertNil(error, "no error encrypting object");
654 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
656 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
658 [self.keychainView waitForFetchAndIncomingQueueProcessing];
660 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
661 (id)kSecReturnAttributes: @YES,
662 (id)kSecAttrSynchronizable: @YES,
663 (id)kSecAttrAccount: @"account-delete-me",
664 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
666 CFTypeRef cfresult = NULL;
667 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
669 NSDictionary* result = CFBridgingRelease(cfresult);
670 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
671 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
672 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
674 // Test that if this item is updated, it remains encrypted in v2
675 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
676 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
677 PCSServiceIdentifier:(NSNumber *)servIdentifier
678 PCSPublicKey:publicKey
679 PCSPublicIdentity:publicIdentity]];
680 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
682 OCMVerifyAllWithDelay(self.mockDatabase, 4);
683 [self waitForCKModifications];
685 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
686 XCTAssertEqualObjects(newRecord[SecCKRecordPCSServiceIdentifier], servIdentifier, "Didn't change service identifier");
687 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicKey], publicKey, "Didn't change public key");
688 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicIdentity], publicIdentity, "Didn't change public identity");
689 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
692 -(void)testResetLocal {
693 // Test starts with nothing in database, but one in our fake CloudKit.
694 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
695 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
696 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
698 // Spin up CKKS subsystem.
699 [self startCKKSSubsystem];
701 // We expect a single record to be uploaded
702 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
703 [self addGenericPassword: @"data" account: @"account-delete-me"];
704 OCMVerifyAllWithDelay(self.mockDatabase, 8);
706 // After the local reset, we expect: a fetch, then nothing
707 self.silentFetchesAllowed = false;
708 [self expectCKFetch];
710 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"];
711 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
712 XCTAssertNil(result, "no error resetting local");
713 [resetExpectation fulfill];
715 [self waitForExpectations:@[resetExpectation] timeout:8.0];
717 OCMVerifyAllWithDelay(self.mockDatabase, 8);
719 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
720 [self addGenericPassword:@"asdf"
721 account:@"account-class-A"
723 access:(id)kSecAttrAccessibleWhenUnlocked
724 expecting:errSecSuccess
725 message:@"Adding class A item"];
726 OCMVerifyAllWithDelay(self.mockDatabase, 8);
729 -(void)testResetLocalWhileLoggedOut {
730 // We're "logged in to" cloudkit but not in circle.
731 self.circleStatus = kSOSCCNotInCircle;
732 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
733 self.silentFetchesAllowed = false;
735 // Test starts with local TLK and key hierarchy in our fake cloudkit
736 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
737 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
739 // Spin up CKKS subsystem.
740 [self startCKKSSubsystem];
742 XCTAssertEqual(0, [self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], "Should have been told of a 'logout' event on startup");
744 NSData* changeTokenData = [[[NSUUID UUID] UUIDString] dataUsingEncoding:NSUTF8StringEncoding];
745 CKServerChangeToken* changeToken = [[CKServerChangeToken alloc] initWithData:changeTokenData];
746 [self.keychainView dispatchSync: ^bool{
747 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
748 ckse.changeToken = changeToken;
750 NSError* error = nil;
751 [ckse saveToDatabase:&error];
752 XCTAssertNil(error, "No error saving new zone state to database");
756 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"];
757 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
758 XCTAssertNil(result, "no error resetting local");
759 secnotice("ckks", "Received a rpcResetLocal callback");
760 [resetExpectation fulfill];
763 [self waitForExpectations:@[resetExpectation] timeout:1.0];
765 [self.keychainView dispatchSync: ^bool{
766 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
767 XCTAssertNotEqualObjects(changeToken, ckse.changeToken, "Change token is reset");
771 // Now log in, and see what happens! It should re-fetch, pick up the old key hierarchy, and use it
772 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
773 self.silentFetchesAllowed = true;
774 self.circleStatus = kSOSCCInCircle;
775 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
777 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
778 [self addGenericPassword:@"asdf"
779 account:@"account-class-A"
781 access:(id)kSecAttrAccessibleWhenUnlocked
782 expecting:errSecSuccess
783 message:@"Adding class A item"];
784 OCMVerifyAllWithDelay(self.mockDatabase, 8);
787 -(void)testResetLocalMultipleTimes {
788 // Test starts with nothing in database, but one in our fake CloudKit.
789 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
790 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
791 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
793 // Spin up CKKS subsystem.
794 [self startCKKSSubsystem];
796 // We expect a single record to be uploaded
797 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready'");
798 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
799 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
800 [self addGenericPassword: @"data" account: @"account-delete-me"];
801 OCMVerifyAllWithDelay(self.mockDatabase, 8);
802 [self waitForCKModifications];
804 // We're going to request a bunch of CloudKit resets, but hold them from finishing
805 [self holdCloudKitFetches];
807 XCTestExpectation* resetExpectation0 = [self expectationWithDescription: @"reset callback(0) occurs"];
808 XCTestExpectation* resetExpectation1 = [self expectationWithDescription: @"reset callback(1) occurs"];
809 XCTestExpectation* resetExpectation2 = [self expectationWithDescription: @"reset callback(2) occurs"];
810 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
811 XCTAssertNil(result, "should receive no error resetting local");
812 secnotice("ckksreset", "Received a rpcResetLocal(0) callback");
813 [resetExpectation0 fulfill];
815 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
816 XCTAssertNil(result, "should receive no error resetting local");
817 secnotice("ckksreset", "Received a rpcResetLocal(1) callback");
818 [resetExpectation1 fulfill];
820 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
821 XCTAssertNil(result, "should receive no error resetting local");
822 secnotice("ckksreset", "Received a rpcResetLocal(2) callback");
823 [resetExpectation2 fulfill];
826 // After the reset(s), we expect no uploads. Let the resets flow!
827 [self releaseCloudKitFetchHold];
828 [self waitForExpectations:@[resetExpectation0, resetExpectation1, resetExpectation2] timeout:20];
829 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready'");
831 OCMVerifyAllWithDelay(self.mockDatabase, 8);
833 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
834 checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
835 [self addGenericPassword:@"asdf"
836 account:@"account-class-A"
838 access:(id)kSecAttrAccessibleWhenUnlocked
839 expecting:errSecSuccess
840 message:@"Adding class A item"];
841 OCMVerifyAllWithDelay(self.mockDatabase, 8);
844 -(void)testResetCloudKitZone {
845 self.silentZoneDeletesAllowed = true;
847 // Test starts with nothing in database, but one in our fake CloudKit.
848 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
849 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
850 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
852 // Spin up CKKS subsystem.
853 [self startCKKSSubsystem];
855 // We expect a single record to be uploaded
856 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
857 [self addGenericPassword: @"data" account: @"account-delete-me"];
858 OCMVerifyAllWithDelay(self.mockDatabase, 8);
859 [self waitForCKModifications];
861 // After the reset, we expect a key hierarchy upload, and then the class C item upload
862 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
863 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
865 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
866 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
867 XCTAssertNil(result, "no error resetting cloudkit");
868 secnotice("ckks", "Received a resetCloudKit callback");
869 [resetExpectation fulfill];
871 [self waitForExpectations:@[resetExpectation] timeout:8.0];
873 OCMVerifyAllWithDelay(self.mockDatabase, 8);
875 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
876 [self addGenericPassword:@"asdf"
877 account:@"account-class-A"
879 access:(id)kSecAttrAccessibleWhenUnlocked
880 expecting:errSecSuccess
881 message:@"Adding class A item"];
882 OCMVerifyAllWithDelay(self.mockDatabase, 8);
885 - (void)testResetCloudKitZoneDuringWaitForTLK {
886 self.silentZoneDeletesAllowed = true;
888 // Test starts with nothing in database, but one in our fake CloudKit.
890 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
891 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
893 // Spin up CKKS subsystem.
894 [self startCKKSSubsystem];
896 // No records should be uploaded
897 [self addGenericPassword: @"data" account: @"account-delete-me"];
899 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS should have entered waitfortlk");
901 // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going)
902 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
903 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
905 CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil];
906 XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold");
908 // Now, reset everything. The outgoingOp should get cancelled.
909 // We expect a key hierarchy upload, and then the class C item upload
910 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
911 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
912 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
914 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
915 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
916 XCTAssertNil(result, "no error resetting cloudkit");
917 [resetExpectation fulfill];
919 [self waitForExpectations:@[resetExpectation] timeout:8.0];
921 XCTAssertTrue([outgoingOp isCancelled], "old stuck ProcessOutgoingQueue should be cancelled");
922 OCMVerifyAllWithDelay(self.mockDatabase, 8);
924 // And adding another item works too
925 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
926 [self addGenericPassword:@"asdf"
927 account:@"account-class-A"
929 access:(id)kSecAttrAccessibleWhenUnlocked
930 expecting:errSecSuccess
931 message:@"Adding class A item"];
932 OCMVerifyAllWithDelay(self.mockDatabase, 8);
936 * This test doesn't work, since the resetLocal fails. CKKS gets back into waitfortlk
937 * but that isn't considered a successful resetLocal.
939 - (void)testResetLocalDuringWaitForTLK {
940 // Test starts with nothing in database, but one in our fake CloudKit.
942 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
944 // Spin up CKKS subsystem.
945 [self startCKKSSubsystem];
947 // No records should be uploaded
948 [self addGenericPassword: @"data" account: @"account-delete-me"];
950 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS should have entered waitfortlk");
952 // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going)
953 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
954 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
956 CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil];
957 XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold");
959 // Now, reset everything. The outgoingOp should get cancelled.
960 // We expect a key hierarchy upload, and then the class C item upload
961 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords:3 zoneID:self.keychainZoneID];
962 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
963 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
965 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
966 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
967 XCTAssertNil(result, "no error resetting local");
968 [resetExpectation fulfill];
970 [self waitForExpectations:@[resetExpectation] timeout:8.0];
972 XCTAssertTrue([outgoingOp isCancelled], "old stuck ProcessOutgoingQueue should be cancelled");
973 OCMVerifyAllWithDelay(self.mockDatabase, 8);
975 // And adding another item works too
976 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
977 [self addGenericPassword:@"asdf"
978 account:@"account-class-A"
980 access:(id)kSecAttrAccessibleWhenUnlocked
981 expecting:errSecSuccess
982 message:@"Adding class A item"];
983 OCMVerifyAllWithDelay(self.mockDatabase, 8);
986 -(void)testResetCloudKitZoneWhileLoggedOut {
987 self.silentZoneDeletesAllowed = true;
989 // We're "logged in to" cloudkit but not in circle.
990 self.circleStatus = kSOSCCNotInCircle;
991 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
992 self.silentFetchesAllowed = false;
994 // Test starts with nothing in database, but one in our fake CloudKit.
995 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
997 // Spin up CKKS subsystem.
998 [self startCKKSSubsystem];
1000 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
1001 [self.keychainZone addToZone: ckr];
1003 XCTAssertNotNil(self.keychainZone.currentDatabase, "Zone exists");
1004 XCTAssertNotNil(self.keychainZone.currentDatabase[ckr.recordID], "An item exists in the fake zone");
1006 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1007 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
1008 XCTAssertNil(result, "no error resetting cloudkit");
1009 secnotice("ckks", "Received a resetCloudKit callback");
1010 [resetExpectation fulfill];
1012 [self waitForExpectations:@[resetExpectation] timeout:1.0];
1014 XCTAssertNil(self.keychainZone.currentDatabase, "No zone anymore!");
1015 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1017 // Now log in, and see what happens! It should create the zone again and upload a whole new key hierarchy
1018 self.silentFetchesAllowed = true;
1019 self.circleStatus = kSOSCCInCircle;
1020 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1022 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1023 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1025 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1026 [self addGenericPassword:@"asdf"
1027 account:@"account-class-A"
1029 access:(id)kSecAttrAccessibleWhenUnlocked
1030 expecting:errSecSuccess
1031 message:@"Adding class A item"];
1032 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1035 - (void)testResetCloudKitZoneMultipleTimes {
1036 self.silentZoneDeletesAllowed = true;
1038 // Test starts with nothing in database, but one in our fake CloudKit.
1039 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1040 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1041 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1043 // Spin up CKKS subsystem.
1044 [self startCKKSSubsystem];
1046 // We expect a single record to be uploaded
1047 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready'");
1048 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1049 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1050 [self addGenericPassword: @"data" account: @"account-delete-me"];
1051 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1052 [self waitForCKModifications];
1054 // We're going to request a bunch of CloudKit resets, but hold them from finishing
1055 [self holdCloudKitFetches];
1057 XCTestExpectation* resetExpectation0 = [self expectationWithDescription: @"reset callback(0) occurs"];
1058 XCTestExpectation* resetExpectation1 = [self expectationWithDescription: @"reset callback(1) occurs"];
1059 XCTestExpectation* resetExpectation2 = [self expectationWithDescription: @"reset callback(2) occurs"];
1060 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
1061 XCTAssertNil(result, "should receive no error resetting cloudkit");
1062 secnotice("ckksreset", "Received a resetCloudKit(0) callback");
1063 [resetExpectation0 fulfill];
1065 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
1066 XCTAssertNil(result, "should receive no error resetting cloudkit");
1067 secnotice("ckksreset", "Received a resetCloudKit(1) callback");
1068 [resetExpectation1 fulfill];
1070 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
1071 XCTAssertNil(result, "should receive no error resetting cloudkit");
1072 secnotice("ckksreset", "Received a resetCloudKit(2) callback");
1073 [resetExpectation2 fulfill];
1076 // After the reset(s), we expect a key hierarchy upload, and then the class C item upload
1077 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
1078 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1079 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1081 // And let the resets flow
1082 [self releaseCloudKitFetchHold];
1083 [self waitForExpectations:@[resetExpectation0, resetExpectation1, resetExpectation2] timeout:20];
1084 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready'");
1086 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1088 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1089 checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1090 [self addGenericPassword:@"asdf"
1091 account:@"account-class-A"
1093 access:(id)kSecAttrAccessibleWhenUnlocked
1094 expecting:errSecSuccess
1095 message:@"Adding class A item"];
1096 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1099 - (void)testRPCFetchAndProcessWhileCloudKitNotResponding {
1100 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1101 [self startCKKSSubsystem];
1103 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready'");
1104 [self holdCloudKitFetches];
1106 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1107 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1108 // done! we should have an underlying error of "fetch isn't working"
1109 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1110 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1111 XCTAssertNotNil(underlying, "Should have received an underlying error");
1112 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1113 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingSuccessfulFetch, "Underlying error should be 'pending fetch'");
1114 [callbackOccurs fulfill];
1117 [self waitForExpectations:@[callbackOccurs] timeout:20.0];
1118 [self releaseCloudKitFetchHold];
1119 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1122 - (void)testRPCFetchAndProcessWhileCloudKitErroring {
1123 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1124 [self startCKKSSubsystem];
1126 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready'");
1128 [self.keychainZone failNextFetchWith:[[CKPrettyError alloc] initWithDomain:CKErrorDomain
1129 code:CKErrorRequestRateLimited
1130 userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:30]}]];
1132 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1133 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1134 // done! we should have an underlying error of "fetch isn't working"
1135 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1136 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1137 XCTAssertNotNil(underlying, "Should have received an underlying error");
1138 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1139 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingSuccessfulFetch, "Underlying error should be 'pending fetch'");
1141 NSError* underunderlying = underlying.userInfo[NSUnderlyingErrorKey];
1142 XCTAssertNotNil(underunderlying, "Should have received another layer of underlying error");
1143 XCTAssertEqualObjects(underunderlying.domain, CKErrorDomain, "Underlying error should be CKErrorDomain");
1144 XCTAssertEqual(underunderlying.code, CKErrorRequestRateLimited, "Underlying error should be 'rate limited'");
1146 [callbackOccurs fulfill];
1149 [self waitForExpectations:@[callbackOccurs] timeout:20.0];
1150 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1153 - (void)testRPCFetchAndProcessWhileInWaitForTLK {
1154 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1155 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1156 [self startCKKSSubsystem];
1158 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
1160 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1161 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1162 // done! we should have an underlying error of "fetch isn't working"
1163 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1164 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1165 XCTAssertNotNil(underlying, "Should have received an underlying error");
1166 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1167 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingKeyReady, "Underlying error should be 'pending key ready'");
1168 [callbackOccurs fulfill];
1171 [self waitForExpectations:@[callbackOccurs] timeout:20.0];
1172 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1175 - (void)testRPCTLKMissingWhenMissing {
1176 // Bring CKKS up in waitfortlk
1177 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1178 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1179 [self startCKKSSubsystem];
1181 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
1183 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1185 [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) {
1186 XCTAssertTrue(missing, "TLKs should be missing");
1187 [callbackOccurs fulfill];
1190 [self waitForExpectations:@[callbackOccurs] timeout:5.0];
1192 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1195 - (void)testRPCTLKMissingWhenFound {
1196 // Bring CKKS up in 'ready'
1197 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1198 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1199 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1200 [self startCKKSSubsystem];
1202 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready''");
1204 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1206 [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) {
1207 XCTAssertFalse(missing, "TLKs should not be missing");
1208 [callbackOccurs fulfill];
1211 [self waitForExpectations:@[callbackOccurs] timeout:5.0];
1213 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1216 - (void)testRPCKnownBadStateWhenTLKsMissing {
1217 // Bring CKKS up in waitfortlk
1218 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1219 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1220 [self startCKKSSubsystem];
1222 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
1224 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1226 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1227 XCTAssertEqual(result, CKKSKnownStateTLKsMissing, "TLKs should be missing");
1228 [callbackOccurs fulfill];
1231 [self waitForExpectations:@[callbackOccurs] timeout:5.0];
1233 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1236 - (void)testRPCKnownBadStateWhenInWaitForUnlock {
1237 // Bring CKKS up in 'waitfortunlok'
1238 self.aksLockState = true;
1239 [self.lockStateTracker recheck];
1240 [self startCKKSSubsystem];
1242 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1243 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:8*NSEC_PER_SEC], @"Key state should get stuck in waitforunlock");
1245 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1247 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1248 XCTAssertEqual(result, CKKSKnownStateWaitForUnlock, "known state should be wait for unlock");
1249 [callbackOccurs fulfill];
1252 [self waitForExpectations:@[callbackOccurs] timeout:5.0];
1254 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1258 - (void)testRPCKnownBadStateWhenInGoodState {
1259 // Bring CKKS up in 'ready'
1260 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1261 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1262 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1263 [self startCKKSSubsystem];
1265 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready''");
1267 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1269 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1270 XCTAssertEqual(result, CKKSKnownStatePossiblyGood, "known state should not be possibly-good");
1271 [callbackOccurs fulfill];
1274 [self waitForExpectations:@[callbackOccurs] timeout:5.0];
1276 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1279 - (void)testRpcStatus {
1280 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1282 [self startCKKSSubsystem];
1284 // Let things shake themselves out.
1285 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1286 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should return to 'ready'");
1287 [self waitForCKModifications];
1289 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1290 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1291 XCTAssertNil(error, "should be no error fetching status for keychain");
1293 // Ugly "global" hack
1294 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1295 NSDictionary* keychainStatus = result[1];
1297 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1298 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1299 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status");
1300 [callbackOccurs fulfill];
1303 [self waitForExpectations:@[callbackOccurs] timeout:5.0];
1306 - (void)testRpcStatusWaitsForAccountDetermination {
1307 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1309 // Set up the account state callbacks to happen in one second
1310 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (1 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
1311 // Let CKKS come up (simulating daemon starting due to RPC)
1312 [self startCKKSSubsystem];
1315 // Before CKKS figures out we're in an account, fire off the status RPC.
1316 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1317 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1318 XCTAssertNil(error, "should be no error fetching status for keychain");
1320 // Ugly "global" hack
1321 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1322 NSDictionary* keychainStatus = result[1];
1324 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1325 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1326 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status");
1327 [callbackOccurs fulfill];
1330 [self waitForExpectations:@[callbackOccurs] timeout:8.0];
1333 - (void)testRpcStatusIsFastDuringError {
1334 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1336 self.keychainFetchError = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecInternalError description:@"injected keychain failure"];
1338 // Let CKKS come up; it should enter 'error'
1339 [self startCKKSSubsystem];
1340 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateError] wait:8*NSEC_PER_SEC], "CKKS entered 'error'");
1342 // Fire off the status RPC; it should return immediately
1343 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1344 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1345 XCTAssertNil(error, "should be no error fetching status for keychain");
1347 // Ugly "global" hack
1348 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1349 NSDictionary* keychainStatus = result[1];
1351 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1352 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1353 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateError, "Should be in 'ready' status");
1354 [callbackOccurs fulfill];
1357 [self waitForExpectations:@[callbackOccurs] timeout:1.0];