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/categories/NSError+UsefulConstructors.h"
37 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
38 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
39 #import "keychain/ckks/CKKS.h"
40 #import "keychain/ckks/CKKSItem.h"
41 #import "keychain/ckks/CKKSItemEncrypter.h"
42 #import "keychain/ckks/CKKSKey.h"
43 #import "keychain/ckks/CKKSViewManager.h"
44 #import "keychain/ckks/CKKSZoneStateEntry.h"
46 #import "keychain/ckks/CKKSControl.h"
47 #import "keychain/ckks/CloudKitCategories.h"
49 #import "keychain/ckks/tests/MockCloudKit.h"
50 #import "keychain/ckks/tests/CKKSTests.h"
51 #import "keychain/ckks/tests/CKKSTests+API.h"
53 @implementation CloudKitKeychainSyncingTestsBase (APITests)
55 -(NSMutableDictionary*)pcsAddItemQuery:(NSString*)account
57 serviceIdentifier:(NSNumber*)serviceIdentifier
58 publicKey:(NSData*)publicKey
59 publicIdentity:(NSData*)publicIdentity
62 (id)kSecClass : (id)kSecClassGenericPassword,
63 (id)kSecReturnPersistentRef: @YES,
64 (id)kSecReturnAttributes: @YES,
65 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
66 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
67 (id)kSecAttrAccount : account,
68 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
69 (id)kSecValueData : data,
70 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
71 (id)kSecAttrPCSPlaintextServiceIdentifier : serviceIdentifier,
72 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
73 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
77 -(NSDictionary*)pcsAddItem:(NSString*)account
79 serviceIdentifier:(NSNumber*)serviceIdentifier
80 publicKey:(NSData*)publicKey
81 publicIdentity:(NSData*)publicIdentity
82 expectingSync:(bool)expectingSync
84 NSMutableDictionary* query = [self pcsAddItemQuery:account
86 serviceIdentifier:(NSNumber*)serviceIdentifier
87 publicKey:(NSData*)publicKey
88 publicIdentity:(NSData*)publicIdentity];
89 CFTypeRef result = NULL;
90 XCTestExpectation* syncExpectation = [self expectationWithDescription: @"callback occurs"];
92 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, &result, ^(bool didSync, CFErrorRef error) {
94 XCTAssertTrue(didSync, "Item synced");
95 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
97 XCTAssertFalse(didSync, "Item did not sync");
98 XCTAssertNotNil((__bridge NSError*)error, "Error syncing item");
101 [syncExpectation fulfill];
102 }), @"_SecItemAddAndNotifyOnSync succeeded");
104 // Verify that the item was written to CloudKit
105 OCMVerifyAllWithDelay(self.mockDatabase, 20);
107 // In real code, you'd need to wait for the _SecItemAddAndNotifyOnSync callback to succeed before proceeding
108 [self waitForExpectations:@[syncExpectation] timeout:20];
110 return (NSDictionary*) CFBridgingRelease(result);
113 - (BOOL (^) (CKRecord*)) checkPCSFieldsBlock: (CKRecordZoneID*) zoneID
114 PCSServiceIdentifier:(NSNumber*)servIdentifier
115 PCSPublicKey:(NSData*)publicKey
116 PCSPublicIdentity:(NSData*)publicIdentity
118 __weak __typeof(self) weakSelf = self;
119 return ^BOOL(CKRecord* record) {
120 __strong __typeof(weakSelf) strongSelf = weakSelf;
121 XCTAssertNotNil(strongSelf, "self exists");
123 XCTAssert([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier], "PCS Service identifier matches input");
124 XCTAssert([record[SecCKRecordPCSPublicKey] isEqual: publicKey], "PCS Public Key matches input");
125 XCTAssert([record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity], "PCS Public Identity matches input");
127 if([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier] &&
128 [record[SecCKRecordPCSPublicKey] isEqual: publicKey] &&
129 [record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity]) {
138 @interface CloudKitKeychainSyncingAPITests : CloudKitKeychainSyncingTestsBase
141 @implementation CloudKitKeychainSyncingAPITests
142 - (void)testSecuritydClientBringup {
144 CFErrorRef cferror = nil;
145 xpc_endpoint_t endpoint = SecCreateSecuritydXPCServerEndpoint(&cferror);
146 XCTAssertNil((__bridge id)cferror, "No error creating securityd endpoint");
147 XCTAssertNotNil(endpoint, "Received securityd endpoint");
150 NSXPCInterface *interface = [NSXPCInterface interfaceWithProtocol:@protocol(SecuritydXPCProtocol)];
151 [SecuritydXPCClient configureSecuritydXPCProtocol: interface];
152 XCTAssertNotNil(interface, "Received a configured CKKS interface");
155 NSXPCListenerEndpoint *listenerEndpoint = [[NSXPCListenerEndpoint alloc] init];
156 [listenerEndpoint _setEndpoint:endpoint];
158 NSXPCConnection* connection = [[NSXPCConnection alloc] initWithListenerEndpoint:listenerEndpoint];
159 XCTAssertNotNil(connection , "Received an active connection");
161 connection.remoteObjectInterface = interface;
165 - (void)testAddAndNotifyOnSync {
166 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
168 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
169 [self startCKKSSubsystem];
171 // Let things shake themselves out.
172 [self.keychainView waitForKeyHierarchyReadiness];
173 [self waitForCKModifications];
175 NSMutableDictionary* query = [@{
176 (id)kSecClass : (id)kSecClassGenericPassword,
177 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
178 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
179 (id)kSecAttrAccount : @"testaccount",
180 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
181 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
184 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
186 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
187 XCTAssertTrue(didSync, "Item synced properly");
188 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
190 [blockExpectation fulfill];
191 }), @"_SecItemAddAndNotifyOnSync succeeded");
193 [self waitForExpectationsWithTimeout:5.0 handler:nil];
196 - (void)testAddAndNotifyOnSyncFailure {
197 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
199 [self startCKKSSubsystem];
200 [self.keychainView waitForFetchAndIncomingQueueProcessing];
202 // Due to item UUID selection, this item will be added with UUID 50184A35-4480-E8BA-769B-567CF72F1EC0.
203 // Add it to CloudKit first!
204 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"50184A35-4480-E8BA-769B-567CF72F1EC0"];
205 [self.keychainZone addToZone: ckr];
209 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
211 NSMutableDictionary* query = [@{
212 (id)kSecClass : (id)kSecClassGenericPassword,
213 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
214 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
215 (id)kSecAttrAccount : @"testaccount",
216 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
217 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
218 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // @ fake view hint for fake view
221 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
223 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
224 XCTAssertFalse(didSync, "Item did not sync (as expected)");
225 XCTAssertNotNil((__bridge NSError*)error, "error exists when item fails to sync");
227 [blockExpectation fulfill];
228 }), @"_SecItemAddAndNotifyOnSync succeeded");
230 [self waitForExpectationsWithTimeout:5.0 handler:nil];
231 [self waitForCKModifications];
234 - (void)testAddAndNotifyOnSyncLoggedOut {
235 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
236 self.accountStatus = CKAccountStatusNoAccount;
237 self.silentFetchesAllowed = false;
238 [self startCKKSSubsystem];
240 XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "CKKS should positively log out");
242 NSMutableDictionary* query = [@{
243 (id)kSecClass : (id)kSecClassGenericPassword,
244 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
245 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
246 (id)kSecAttrAccount : @"testaccount",
247 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
248 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
251 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
253 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
254 XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)");
255 XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out");
257 [blockExpectation fulfill];
258 }), @"_SecItemAddAndNotifyOnSync succeeded");
260 [self waitForExpectationsWithTimeout:5.0 handler:nil];
263 - (void)testAddAndNotifyOnSyncAccountStatusUnclear {
264 // Test starts with nothing in database, but CKKS hasn't been told we've logged out yet.
265 // We expect no CKKS operations.
266 self.accountStatus = CKAccountStatusNoAccount;
267 self.silentFetchesAllowed = false;
269 NSMutableDictionary* query = [@{
270 (id)kSecClass : (id)kSecClassGenericPassword,
271 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
272 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
273 (id)kSecAttrAccount : @"testaccount",
274 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
275 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
278 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
280 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
281 XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)");
282 XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out");
284 [blockExpectation fulfill];
285 }), @"_SecItemAddAndNotifyOnSync succeeded");
287 // And now, allow CKKS to discover we're logged out
288 [self startCKKSSubsystem];
289 XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "CKKS should positively log out");
291 [self waitForExpectationsWithTimeout:5.0 handler:nil];
294 - (void)testAddAndNotifyOnSyncBeforeKeyHierarchyReady {
295 // Test starts with a key hierarchy in cloudkit and the TLK having arrived
296 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
297 [self saveTLKMaterialToKeychain:self.keychainZoneID];
298 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
300 // But block CloudKit fetches (so the key hierarchy won't be ready when we add this new item)
301 [self holdCloudKitFetches];
303 [self startCKKSSubsystem];
304 XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "CKKS should log in");
305 [self.keychainView.zoneSetupOperation waitUntilFinished];
307 NSMutableDictionary* query = [@{
308 (id)kSecClass : (id)kSecClassGenericPassword,
309 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
310 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
311 (id)kSecAttrAccount : @"testaccount",
312 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
313 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
314 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
317 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
319 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
320 XCTAssertTrue(didSync, "Item synced");
321 XCTAssertNil((__bridge NSError*)error, "Shouldn't have received an error syncing item");
323 [blockExpectation fulfill];
324 }), @"_SecItemAddAndNotifyOnSync succeeded");
326 // We should be in the 'fetch' state, but no further
327 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateFetch] wait:20*NSEC_PER_SEC], @"Should have reached key state 'fetch', but no further");
329 // When we release the fetch, the callback should still fire and the item should upload
330 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
331 [self releaseCloudKitFetchHold];
332 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Should have reached key state 'ready'");
334 // Verify that the item was written to CloudKit
335 OCMVerifyAllWithDelay(self.mockDatabase, 20);
337 [self waitForExpectationsWithTimeout:5.0 handler:nil];
340 - (void)testPCSUnencryptedFieldsAdd {
341 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
343 [self startCKKSSubsystem];
344 [self.keychainView waitForKeyHierarchyReadiness];
346 NSNumber* servIdentifier = @3;
347 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
348 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
350 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
351 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
352 PCSServiceIdentifier:(NSNumber *)servIdentifier
353 PCSPublicKey:publicKey
354 PCSPublicIdentity:publicIdentity]];
356 NSMutableDictionary* query = [@{
357 (id)kSecClass : (id)kSecClassGenericPassword,
358 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
359 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
360 (id)kSecAttrAccount : @"testaccount",
361 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
362 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
363 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
364 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
365 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
366 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
367 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // allows a CKKSScanOperation to find this item
370 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
372 // Verify that the item is written to CloudKit
373 OCMVerifyAllWithDelay(self.mockDatabase, 20);
375 CFTypeRef item = NULL;
376 query[(id)kSecValueData] = nil;
377 query[(id)kSecReturnAttributes] = @YES;
378 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
380 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
381 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Service Identifier exists");
382 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key exists");
383 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity exists");
385 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
386 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
387 [self waitForCKModifications];
388 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
389 CKRecord* record = self.keychainZone.currentDatabase[recordID];
390 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
392 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], servIdentifier, "Service identifier sent to cloudkit");
393 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], publicKey, "public key sent to cloudkit");
394 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], publicIdentity, "public identity sent to cloudkit");
397 - (void)testPCSUnencryptedFieldsModify {
398 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
400 [self startCKKSSubsystem];
401 [self.keychainView waitForKeyHierarchyReadiness];
403 NSNumber* servIdentifier = @3;
404 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
405 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
407 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
408 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
409 PCSServiceIdentifier:(NSNumber *)servIdentifier
410 PCSPublicKey:publicKey
411 PCSPublicIdentity:publicIdentity]];
413 NSMutableDictionary* query = [@{
414 (id)kSecClass : (id)kSecClassGenericPassword,
415 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
416 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
417 (id)kSecAttrAccount : @"testaccount",
418 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
419 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
420 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
421 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
422 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
423 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
424 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // allows a CKKSScanOperation to find this item
427 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
429 OCMVerifyAllWithDelay(self.mockDatabase, 20);
430 [self waitForCKModifications];
432 query[(id)kSecValueData] = nil;
433 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
434 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
435 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
438 publicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
440 NSNumber* newServiceIdentifier = @10;
441 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
442 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
444 NSDictionary* update = @{
445 (id)kSecAttrPCSPlaintextServiceIdentifier : newServiceIdentifier,
446 (id)kSecAttrPCSPlaintextPublicKey : newPublicKey,
447 (id)kSecAttrPCSPlaintextPublicIdentity : newPublicIdentity,
450 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
451 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
452 PCSServiceIdentifier:(NSNumber *)newServiceIdentifier
453 PCSPublicKey:newPublicKey
454 PCSPublicIdentity:newPublicIdentity]];
456 XCTAssertEqual(errSecSuccess, SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update), @"SecItemUpdate succeeded");
457 OCMVerifyAllWithDelay(self.mockDatabase, 20);
459 CFTypeRef item = NULL;
460 query[(id)kSecValueData] = nil;
461 query[(id)kSecReturnAttributes] = @YES;
462 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
464 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
465 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], newServiceIdentifier, "Service Identifier exists");
466 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], newPublicKey, "public key exists");
467 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], newPublicIdentity, "public identity exists");
469 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
470 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
471 [self waitForCKModifications];
472 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
473 CKRecord* record = self.keychainZone.currentDatabase[recordID];
474 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
476 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], newServiceIdentifier, "Service identifier sent to cloudkit");
477 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], newPublicKey, "public key sent to cloudkit");
478 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], newPublicIdentity, "public identity sent to cloudkit");
481 // As of [<rdar://problem/32558310> CKKS: Re-authenticate PCSPublicFields], these fields are NOT server-modifiable. This test proves it.
482 - (void)testPCSUnencryptedFieldsServerModifyFail {
483 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
485 [self startCKKSSubsystem];
486 [self.keychainView waitForKeyHierarchyReadiness];
488 NSNumber* servIdentifier = @3;
489 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
490 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
492 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
493 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
494 PCSServiceIdentifier:(NSNumber *)servIdentifier
495 PCSPublicKey:publicKey
496 PCSPublicIdentity:publicIdentity]];
498 NSMutableDictionary* query = [@{
499 (id)kSecClass : (id)kSecClassGenericPassword,
500 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
501 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
502 (id)kSecAttrAccount : @"testaccount",
503 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
504 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
505 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
506 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
507 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
508 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
509 (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // fake, for CKKSScanOperation
512 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
514 OCMVerifyAllWithDelay(self.mockDatabase, 20);
515 [self waitForCKModifications];
517 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
518 // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
519 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
520 CKRecord* record = self.keychainZone.currentDatabase[recordID];
521 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
523 // Items are encrypted using encv2
524 XCTAssertEqualObjects(record[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
527 // Test has already failed; find the record just to be nice.
528 for(CKRecord* maybe in self.keychainZone.currentDatabase.allValues) {
529 if(maybe[SecCKRecordPCSServiceIdentifier] != nil) {
535 NSNumber* newServiceIdentifier = @10;
536 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
537 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
539 // Change the public key and public identity
540 record = [record copyWithZone: nil];
541 record[SecCKRecordPCSServiceIdentifier] = newServiceIdentifier;
542 record[SecCKRecordPCSPublicKey] = newPublicKey;
543 record[SecCKRecordPCSPublicIdentity] = newPublicIdentity;
544 [self.keychainZone addToZone: record];
546 // Trigger a notification
547 [self.keychainView notifyZoneChange:nil];
548 [self.keychainView waitForFetchAndIncomingQueueProcessing];
550 CFTypeRef item = NULL;
551 query[(id)kSecValueData] = nil;
552 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
553 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
554 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
555 query[(id)kSecReturnAttributes] = @YES;
556 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
558 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
559 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "service identifier is not updated");
560 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key not updated");
561 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity not updated");
564 -(void)testPCSUnencryptedFieldsRecieveUnauthenticatedFields {
565 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
567 [self startCKKSSubsystem];
568 [self.keychainView waitForKeyHierarchyReadiness];
570 NSNumber* servIdentifier = @3;
571 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
572 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
574 NSError* error = nil;
576 // Manually encrypt an item
577 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
578 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
579 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
580 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
581 parentKeyUUID:self.keychainZoneKeys.classC.uuid
582 zoneID:recordID.zoneID];
583 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
584 XCTAssertNotNil(itemkey, "Got a key");
585 cipheritem.wrappedkey = itemkey.wrappedkey;
586 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
588 cipheritem.encver = CKKSItemEncryptionVersion1;
590 // This item has the PCS public fields, but they are not authenticated
591 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
592 cipheritem.plaintextPCSPublicKey = publicKey;
593 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
595 NSDictionary<NSString*, NSData*>* authenticatedData = [cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion1];
596 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
597 XCTAssertNil(error, "no error encrypting object");
598 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
600 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
602 [self.keychainView waitForFetchAndIncomingQueueProcessing];
604 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
605 (id)kSecReturnAttributes: @YES,
606 (id)kSecAttrSynchronizable: @YES,
607 (id)kSecAttrAccount: @"account-delete-me",
608 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
610 CFTypeRef cfresult = NULL;
611 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
613 NSDictionary* result = CFBridgingRelease(cfresult);
614 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
615 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
616 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
619 -(void)testPCSUnencryptedFieldsRecieveAuthenticatedFields {
620 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
622 [self startCKKSSubsystem];
623 [self.keychainView waitForKeyHierarchyReadiness];
624 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
626 NSNumber* servIdentifier = @3;
627 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
628 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
630 NSError* error = nil;
632 // Manually encrypt an item
633 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
634 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
635 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
636 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
637 parentKeyUUID:self.keychainZoneKeys.classC.uuid
638 zoneID:recordID.zoneID];
639 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
640 XCTAssertNotNil(itemkey, "Got a key");
641 cipheritem.wrappedkey = itemkey.wrappedkey;
642 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
644 cipheritem.encver = CKKSItemEncryptionVersion2;
646 // This item has the PCS public fields, and they are authenticated (since we're using v2)
647 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
648 cipheritem.plaintextPCSPublicKey = publicKey;
649 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
651 // Use version 2, so PCS plaintext fields will be authenticated
652 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
654 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
655 XCTAssertNil(error, "no error encrypting object");
656 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
658 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
660 [self.keychainView waitForFetchAndIncomingQueueProcessing];
662 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
663 (id)kSecReturnAttributes: @YES,
664 (id)kSecAttrSynchronizable: @YES,
665 (id)kSecAttrAccount: @"account-delete-me",
666 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
668 CFTypeRef cfresult = NULL;
669 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
671 NSDictionary* result = CFBridgingRelease(cfresult);
672 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
673 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
674 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
676 // Test that if this item is updated, it remains encrypted in v2
677 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
678 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
679 PCSServiceIdentifier:(NSNumber *)servIdentifier
680 PCSPublicKey:publicKey
681 PCSPublicIdentity:publicIdentity]];
682 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
684 OCMVerifyAllWithDelay(self.mockDatabase, 20);
685 [self waitForCKModifications];
687 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
688 XCTAssertEqualObjects(newRecord[SecCKRecordPCSServiceIdentifier], servIdentifier, "Didn't change service identifier");
689 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicKey], publicKey, "Didn't change public key");
690 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicIdentity], publicIdentity, "Didn't change public identity");
691 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
694 -(void)testResetLocal {
695 // Test starts with nothing in database, but one in our fake CloudKit.
696 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
697 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
698 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
700 // Spin up CKKS subsystem.
701 [self startCKKSSubsystem];
703 // We expect a single record to be uploaded
704 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
705 [self addGenericPassword: @"data" account: @"account-delete-me"];
706 OCMVerifyAllWithDelay(self.mockDatabase, 20);
708 // After the local reset, we expect: a fetch, then nothing
709 self.silentFetchesAllowed = false;
710 [self expectCKFetch];
712 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"];
713 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
714 XCTAssertNil(result, "no error resetting local");
715 [resetExpectation fulfill];
717 [self waitForExpectations:@[resetExpectation] timeout:20];
719 OCMVerifyAllWithDelay(self.mockDatabase, 20);
721 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
722 [self addGenericPassword:@"asdf"
723 account:@"account-class-A"
725 access:(id)kSecAttrAccessibleWhenUnlocked
726 expecting:errSecSuccess
727 message:@"Adding class A item"];
728 OCMVerifyAllWithDelay(self.mockDatabase, 20);
731 -(void)testResetLocalWhileLoggedOut {
732 // We're "logged in to" cloudkit but not in circle.
733 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCNotInCircle error:nil];
734 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
735 self.silentFetchesAllowed = false;
737 // Test starts with local TLK and key hierarchy in our fake cloudkit
738 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
739 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
741 // Spin up CKKS subsystem.
742 [self startCKKSSubsystem];
744 XCTAssertEqual(0, [self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], "Should have been told of a 'logout' event on startup");
746 NSData* changeTokenData = [[[NSUUID UUID] UUIDString] dataUsingEncoding:NSUTF8StringEncoding];
747 CKServerChangeToken* changeToken = [[CKServerChangeToken alloc] initWithData:changeTokenData];
748 [self.keychainView dispatchSync: ^bool{
749 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
750 ckse.changeToken = changeToken;
752 NSError* error = nil;
753 [ckse saveToDatabase:&error];
754 XCTAssertNil(error, "No error saving new zone state to database");
758 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"];
759 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
760 XCTAssertNil(result, "no error resetting local");
761 secnotice("ckks", "Received a rpcResetLocal callback");
762 [resetExpectation fulfill];
765 [self waitForExpectations:@[resetExpectation] timeout:20];
767 [self.keychainView dispatchSync: ^bool{
768 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
769 XCTAssertNotEqualObjects(changeToken, ckse.changeToken, "Change token is reset");
773 // Now log in, and see what happens! It should re-fetch, pick up the old key hierarchy, and use it
774 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
775 self.silentFetchesAllowed = true;
776 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];;
777 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
779 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
780 [self addGenericPassword:@"asdf"
781 account:@"account-class-A"
783 access:(id)kSecAttrAccessibleWhenUnlocked
784 expecting:errSecSuccess
785 message:@"Adding class A item"];
786 OCMVerifyAllWithDelay(self.mockDatabase, 20);
789 -(void)testResetLocalMultipleTimes {
790 // Test starts with nothing in database, but one in our fake CloudKit.
791 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
792 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
793 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
795 // Spin up CKKS subsystem.
796 [self startCKKSSubsystem];
798 // We expect a single record to be uploaded
799 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
800 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
801 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
802 [self addGenericPassword: @"data" account: @"account-delete-me"];
803 OCMVerifyAllWithDelay(self.mockDatabase, 20);
804 [self waitForCKModifications];
806 // We're going to request a bunch of CloudKit resets, but hold them from finishing
807 [self holdCloudKitFetches];
809 XCTestExpectation* resetExpectation0 = [self expectationWithDescription: @"reset callback(0) occurs"];
810 XCTestExpectation* resetExpectation1 = [self expectationWithDescription: @"reset callback(1) occurs"];
811 XCTestExpectation* resetExpectation2 = [self expectationWithDescription: @"reset callback(2) occurs"];
812 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
813 XCTAssertNil(result, "should receive no error resetting local");
814 secnotice("ckksreset", "Received a rpcResetLocal(0) callback");
815 [resetExpectation0 fulfill];
817 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
818 XCTAssertNil(result, "should receive no error resetting local");
819 secnotice("ckksreset", "Received a rpcResetLocal(1) callback");
820 [resetExpectation1 fulfill];
822 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
823 XCTAssertNil(result, "should receive no error resetting local");
824 secnotice("ckksreset", "Received a rpcResetLocal(2) callback");
825 [resetExpectation2 fulfill];
828 // After the reset(s), we expect no uploads. Let the resets flow!
829 [self releaseCloudKitFetchHold];
830 [self waitForExpectations:@[resetExpectation0, resetExpectation1, resetExpectation2] timeout:20];
831 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
833 OCMVerifyAllWithDelay(self.mockDatabase, 20);
835 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
836 checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
837 [self addGenericPassword:@"asdf"
838 account:@"account-class-A"
840 access:(id)kSecAttrAccessibleWhenUnlocked
841 expecting:errSecSuccess
842 message:@"Adding class A item"];
843 OCMVerifyAllWithDelay(self.mockDatabase, 20);
846 -(void)testResetCloudKitZone {
847 self.silentZoneDeletesAllowed = true;
849 // Test starts with nothing in database, but one in our fake CloudKit.
850 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
851 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
852 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
854 // Spin up CKKS subsystem.
855 [self startCKKSSubsystem];
857 // We expect a single record to be uploaded
858 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
859 [self addGenericPassword: @"data" account: @"account-delete-me"];
860 OCMVerifyAllWithDelay(self.mockDatabase, 20);
861 [self waitForCKModifications];
863 // After the reset, we expect a key hierarchy upload, and then the class C item upload
864 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
865 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
867 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
868 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
869 XCTAssertNil(result, "no error resetting cloudkit");
870 secnotice("ckks", "Received a resetCloudKit callback");
871 [resetExpectation fulfill];
873 [self waitForExpectations:@[resetExpectation] timeout:20];
875 OCMVerifyAllWithDelay(self.mockDatabase, 20);
877 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
878 [self addGenericPassword:@"asdf"
879 account:@"account-class-A"
881 access:(id)kSecAttrAccessibleWhenUnlocked
882 expecting:errSecSuccess
883 message:@"Adding class A item"];
884 OCMVerifyAllWithDelay(self.mockDatabase, 20);
887 - (void)testResetCloudKitZoneDuringWaitForTLK {
888 self.silentZoneDeletesAllowed = true;
890 // Test starts with nothing in database, but one in our fake CloudKit.
892 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
893 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
895 // Spin up CKKS subsystem.
896 [self startCKKSSubsystem];
898 // No records should be uploaded
899 [self addGenericPassword: @"data" account: @"account-delete-me"];
901 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS should have entered waitfortlk");
903 // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going)
904 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
905 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
907 CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil];
908 XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold");
910 // Now, reset everything. The outgoingOp should get cancelled.
911 // We expect a key hierarchy upload, and then the class C item upload
912 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
913 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
914 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
916 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
917 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
918 XCTAssertNil(result, "no error resetting cloudkit");
919 [resetExpectation fulfill];
921 [self waitForExpectations:@[resetExpectation] timeout:20];
923 XCTAssertTrue([outgoingOp isCancelled], "old stuck ProcessOutgoingQueue should be cancelled");
924 OCMVerifyAllWithDelay(self.mockDatabase, 20);
926 // And adding another item works too
927 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
928 [self addGenericPassword:@"asdf"
929 account:@"account-class-A"
931 access:(id)kSecAttrAccessibleWhenUnlocked
932 expecting:errSecSuccess
933 message:@"Adding class A item"];
934 OCMVerifyAllWithDelay(self.mockDatabase, 20);
938 * This test doesn't work, since the resetLocal fails. CKKS gets back into waitfortlk
939 * but that isn't considered a successful resetLocal.
941 - (void)testResetLocalDuringWaitForTLK {
942 // Test starts with nothing in database, but one in our fake CloudKit.
944 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
946 // Spin up CKKS subsystem.
947 [self startCKKSSubsystem];
949 // No records should be uploaded
950 [self addGenericPassword: @"data" account: @"account-delete-me"];
952 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS should have entered waitfortlk");
954 // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going)
955 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
956 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
958 CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil];
959 XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold");
961 // Now, reset everything. The outgoingOp should get cancelled.
962 // We expect a key hierarchy upload, and then the class C item upload
963 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords:3 zoneID:self.keychainZoneID];
964 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
965 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
967 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
968 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
969 XCTAssertNil(result, "no error resetting local");
970 [resetExpectation fulfill];
972 [self waitForExpectations:@[resetExpectation] timeout:20];
974 XCTAssertTrue([outgoingOp isCancelled], "old stuck ProcessOutgoingQueue should be cancelled");
975 OCMVerifyAllWithDelay(self.mockDatabase, 20);
977 // And adding another item works too
978 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
979 [self addGenericPassword:@"asdf"
980 account:@"account-class-A"
982 access:(id)kSecAttrAccessibleWhenUnlocked
983 expecting:errSecSuccess
984 message:@"Adding class A item"];
985 OCMVerifyAllWithDelay(self.mockDatabase, 20);
988 -(void)testResetCloudKitZoneWhileLoggedOut {
989 self.silentZoneDeletesAllowed = true;
991 // We're "logged in to" cloudkit but not in circle.
992 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCNotInCircle error:nil];;
993 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
994 self.silentFetchesAllowed = false;
996 // Test starts with nothing in database, but one in our fake CloudKit.
997 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
999 // Spin up CKKS subsystem.
1000 [self startCKKSSubsystem];
1002 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
1003 [self.keychainZone addToZone: ckr];
1005 XCTAssertNotNil(self.keychainZone.currentDatabase, "Zone exists");
1006 XCTAssertNotNil(self.keychainZone.currentDatabase[ckr.recordID], "An item exists in the fake zone");
1008 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
1009 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1010 XCTAssertNil(result, "no error resetting cloudkit");
1011 secnotice("ckks", "Received a resetCloudKit callback");
1012 [resetExpectation fulfill];
1014 [self waitForExpectations:@[resetExpectation] timeout:20];
1016 XCTAssertNil(self.keychainZone.currentDatabase, "No zone anymore!");
1017 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1019 // Now log in, and see what happens! It should create the zone again and upload a whole new key hierarchy
1020 self.silentFetchesAllowed = true;
1021 self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];;
1022 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
1024 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
1025 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1027 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1028 [self addGenericPassword:@"asdf"
1029 account:@"account-class-A"
1031 access:(id)kSecAttrAccessibleWhenUnlocked
1032 expecting:errSecSuccess
1033 message:@"Adding class A item"];
1034 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1037 - (void)testResetCloudKitZoneMultipleTimes {
1038 self.silentZoneDeletesAllowed = true;
1040 // Test starts with nothing in database, but one in our fake CloudKit.
1041 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1042 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1043 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
1045 // Spin up CKKS subsystem.
1046 [self startCKKSSubsystem];
1048 // We expect a single record to be uploaded
1049 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1050 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1051 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1052 [self addGenericPassword: @"data" account: @"account-delete-me"];
1053 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1054 [self waitForCKModifications];
1056 // We're going to request a bunch of CloudKit resets, but hold them from finishing
1057 [self holdCloudKitFetches];
1059 XCTestExpectation* resetExpectation0 = [self expectationWithDescription: @"reset callback(0) occurs"];
1060 XCTestExpectation* resetExpectation1 = [self expectationWithDescription: @"reset callback(1) occurs"];
1061 XCTestExpectation* resetExpectation2 = [self expectationWithDescription: @"reset callback(2) occurs"];
1062 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1063 XCTAssertNil(result, "should receive no error resetting cloudkit");
1064 secnotice("ckksreset", "Received a resetCloudKit(0) callback");
1065 [resetExpectation0 fulfill];
1067 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1068 XCTAssertNil(result, "should receive no error resetting cloudkit");
1069 secnotice("ckksreset", "Received a resetCloudKit(1) callback");
1070 [resetExpectation1 fulfill];
1072 [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) {
1073 XCTAssertNil(result, "should receive no error resetting cloudkit");
1074 secnotice("ckksreset", "Received a resetCloudKit(2) callback");
1075 [resetExpectation2 fulfill];
1078 // After the reset(s), we expect a key hierarchy upload, and then the class C item upload
1079 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
1080 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1081 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
1083 // And let the resets flow
1084 [self releaseCloudKitFetchHold];
1085 [self waitForExpectations:@[resetExpectation0, resetExpectation1, resetExpectation2] timeout:20];
1086 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1088 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1090 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
1091 checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
1092 [self addGenericPassword:@"asdf"
1093 account:@"account-class-A"
1095 access:(id)kSecAttrAccessibleWhenUnlocked
1096 expecting:errSecSuccess
1097 message:@"Adding class A item"];
1098 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1101 - (void)testRPCFetchAndProcessWhileCloudKitNotResponding {
1102 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1103 [self startCKKSSubsystem];
1105 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1106 [self holdCloudKitFetches];
1108 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1109 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1110 // done! we should have an underlying error of "fetch isn't working"
1111 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1112 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1113 XCTAssertNotNil(underlying, "Should have received an underlying error");
1114 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1115 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingSuccessfulFetch, "Underlying error should be 'pending fetch'");
1116 [callbackOccurs fulfill];
1119 [self waitForExpectations:@[callbackOccurs] timeout:20];
1120 [self releaseCloudKitFetchHold];
1121 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1124 - (void)testRPCFetchAndProcessWhileCloudKitErroring {
1125 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1126 [self startCKKSSubsystem];
1128 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'");
1130 [self.keychainZone failNextFetchWith:[[CKPrettyError alloc] initWithDomain:CKErrorDomain
1131 code:CKErrorRequestRateLimited
1132 userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:30]}]];
1134 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1135 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1136 // done! we should have an underlying error of "fetch isn't working"
1137 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1138 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1139 XCTAssertNotNil(underlying, "Should have received an underlying error");
1140 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1141 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingSuccessfulFetch, "Underlying error should be 'pending fetch'");
1143 NSError* underunderlying = underlying.userInfo[NSUnderlyingErrorKey];
1144 XCTAssertNotNil(underunderlying, "Should have received another layer of underlying error");
1145 XCTAssertEqualObjects(underunderlying.domain, CKErrorDomain, "Underlying error should be CKErrorDomain");
1146 XCTAssertEqual(underunderlying.code, CKErrorRequestRateLimited, "Underlying error should be 'rate limited'");
1148 [callbackOccurs fulfill];
1151 [self waitForExpectations:@[callbackOccurs] timeout:20];
1152 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1155 - (void)testRPCFetchAndProcessWhileInWaitForTLK {
1156 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1157 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1158 [self startCKKSSubsystem];
1160 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1162 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1163 [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) {
1164 // done! we should have an underlying error of "fetch isn't working"
1165 XCTAssertNotNil(error, "Should have received an error attempting to fetch and process");
1166 NSError* underlying = error.userInfo[NSUnderlyingErrorKey];
1167 XCTAssertNotNil(underlying, "Should have received an underlying error");
1168 XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain");
1169 XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingKeyReady, "Underlying error should be 'pending key ready'");
1170 [callbackOccurs fulfill];
1173 [self waitForExpectations:@[callbackOccurs] timeout:20];
1174 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1177 - (void)testRPCTLKMissingWhenMissing {
1178 // Bring CKKS up in waitfortlk
1179 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1180 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1181 [self startCKKSSubsystem];
1183 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1185 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1187 [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) {
1188 XCTAssertTrue(missing, "TLKs should be missing");
1189 [callbackOccurs fulfill];
1192 [self waitForExpectations:@[callbackOccurs] timeout:20];
1194 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1197 - (void)testRPCTLKMissingWhenFound {
1198 // Bring CKKS up in 'ready'
1199 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1200 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1201 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1202 [self startCKKSSubsystem];
1204 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready''");
1206 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1208 [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) {
1209 XCTAssertFalse(missing, "TLKs should not be missing");
1210 [callbackOccurs fulfill];
1213 [self waitForExpectations:@[callbackOccurs] timeout:20];
1215 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1218 - (void)testRPCKnownBadStateWhenTLKsMissing {
1219 // Bring CKKS up in waitfortlk
1220 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1221 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
1222 [self startCKKSSubsystem];
1224 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
1226 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1228 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1229 XCTAssertEqual(result, CKKSKnownStateTLKsMissing, "TLKs should be missing");
1230 [callbackOccurs fulfill];
1233 [self waitForExpectations:@[callbackOccurs] timeout:20];
1235 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1238 - (void)testRPCKnownBadStateWhenInWaitForUnlock {
1239 // Bring CKKS up in 'waitfortunlok'
1240 self.aksLockState = true;
1241 [self.lockStateTracker recheck];
1242 [self startCKKSSubsystem];
1244 // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
1245 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitforunlock");
1247 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1249 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1250 XCTAssertEqual(result, CKKSKnownStateWaitForUnlock, "known state should be wait for unlock");
1251 [callbackOccurs fulfill];
1254 [self waitForExpectations:@[callbackOccurs] timeout:20];
1256 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1260 - (void)testRPCKnownBadStateWhenInGoodState {
1261 // Bring CKKS up in 'ready'
1262 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1263 [self saveTLKMaterialToKeychain:self.keychainZoneID];
1264 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
1265 [self startCKKSSubsystem];
1267 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready''");
1269 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1271 [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) {
1272 XCTAssertEqual(result, CKKSKnownStatePossiblyGood, "known state should not be possibly-good");
1273 [callbackOccurs fulfill];
1276 [self waitForExpectations:@[callbackOccurs] timeout:20];
1278 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1281 - (void)testRpcStatus {
1282 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1284 [self startCKKSSubsystem];
1286 // Let things shake themselves out.
1287 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1288 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should return to 'ready'");
1289 [self waitForCKModifications];
1291 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1292 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1293 XCTAssertNil(error, "should be no error fetching status for keychain");
1295 // Ugly "global" hack
1296 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1297 NSDictionary* keychainStatus = result[1];
1299 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1300 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1301 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status");
1302 [callbackOccurs fulfill];
1305 [self waitForExpectations:@[callbackOccurs] timeout:20];
1308 - (void)testRpcStatusWaitsForAccountDetermination {
1309 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1311 // Set up the account state callbacks to happen in one second
1312 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (1 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
1313 // Let CKKS come up (simulating daemon starting due to RPC)
1314 [self startCKKSSubsystem];
1317 // Before CKKS figures out we're in an account, fire off the status RPC.
1318 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1319 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1320 XCTAssertNil(error, "should be no error fetching status for keychain");
1322 // Ugly "global" hack
1323 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1324 NSDictionary* keychainStatus = result[1];
1326 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1327 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1328 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status");
1329 [callbackOccurs fulfill];
1332 [self waitForExpectations:@[callbackOccurs] timeout:20];
1335 - (void)testRpcStatusIsFastDuringError {
1336 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
1338 self.keychainFetchError = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecInternalError description:@"injected keychain failure"];
1340 // Let CKKS come up; it should enter 'error'
1341 [self startCKKSSubsystem];
1342 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateError] wait:20*NSEC_PER_SEC], "CKKS entered 'error'");
1344 // Fire off the status RPC; it should return immediately
1345 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1346 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1347 XCTAssertNil(error, "should be no error fetching status for keychain");
1349 // Ugly "global" hack
1350 XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back");
1351 NSDictionary* keychainStatus = result[1];
1353 XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back");
1354 XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view");
1355 XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateError, "Should be in 'ready' status");
1356 [callbackOccurs fulfill];
1359 [self waitForExpectations:@[callbackOccurs] timeout:20];