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"
46 #import "keychain/ckks/tests/MockCloudKit.h"
47 #import "keychain/ckks/tests/CKKSTests.h"
48 #import "keychain/ckks/tests/CKKSTests+API.h"
50 @implementation CloudKitKeychainSyncingTests (APITests)
52 - (void)testSecuritydClientBringup {
53 CFErrorRef cferror = nil;
54 xpc_endpoint_t endpoint = SecCreateSecuritydXPCServerEndpoint(&cferror);
55 XCTAssertNil((__bridge id)cferror, "No error creating securityd endpoint");
56 XCTAssertNotNil(endpoint, "Received securityd endpoint");
58 NSXPCInterface *interface = [NSXPCInterface interfaceWithProtocol:@protocol(SecuritydXPCProtocol)];
59 [SecuritydXPCClient configureSecuritydXPCProtocol: interface];
60 XCTAssertNotNil(interface, "Received a configured CKKS interface");
62 NSXPCListenerEndpoint *listenerEndpoint = [[NSXPCListenerEndpoint alloc] init];
63 [listenerEndpoint _setEndpoint:endpoint];
65 NSXPCConnection* connection = [[NSXPCConnection alloc] initWithListenerEndpoint:listenerEndpoint];
66 XCTAssertNotNil(connection , "Received an active connection");
68 connection.remoteObjectInterface = interface;
71 -(NSMutableDictionary*)pcsAddItemQuery:(NSString*)account
73 serviceIdentifier:(NSNumber*)serviceIdentifier
74 publicKey:(NSData*)publicKey
75 publicIdentity:(NSData*)publicIdentity
78 (id)kSecClass : (id)kSecClassGenericPassword,
79 (id)kSecReturnPersistentRef: @YES,
80 (id)kSecReturnAttributes: @YES,
81 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
82 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
83 (id)kSecAttrAccount : account,
84 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
85 (id)kSecValueData : data,
86 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
87 (id)kSecAttrPCSPlaintextServiceIdentifier : serviceIdentifier,
88 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
89 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
93 -(NSDictionary*)pcsAddItem:(NSString*)account
95 serviceIdentifier:(NSNumber*)serviceIdentifier
96 publicKey:(NSData*)publicKey
97 publicIdentity:(NSData*)publicIdentity
98 expectingSync:(bool)expectingSync
100 NSMutableDictionary* query = [self pcsAddItemQuery:account
102 serviceIdentifier:(NSNumber*)serviceIdentifier
103 publicKey:(NSData*)publicKey
104 publicIdentity:(NSData*)publicIdentity];
105 CFTypeRef result = NULL;
106 XCTestExpectation* syncExpectation = [self expectationWithDescription: @"callback occurs"];
108 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, &result, ^(bool didSync, CFErrorRef error) {
110 XCTAssertTrue(didSync, "Item synced");
111 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
113 XCTAssertFalse(didSync, "Item did not sync");
114 XCTAssertNotNil((__bridge NSError*)error, "Error syncing item");
117 [syncExpectation fulfill];
118 }), @"_SecItemAddAndNotifyOnSync succeeded");
120 // Verify that the item was written to CloudKit
121 OCMVerifyAllWithDelay(self.mockDatabase, 8);
123 // In real code, you'd need to wait for the _SecItemAddAndNotifyOnSync callback to succeed before proceeding
124 [self waitForExpectations:@[syncExpectation] timeout:8.0];
126 return (NSDictionary*) CFBridgingRelease(result);
130 - (void)testAddAndNotifyOnSync {
131 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
133 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
134 [self startCKKSSubsystem];
136 // Let things shake themselves out.
137 [self.keychainView waitForKeyHierarchyReadiness];
138 [self waitForCKModifications];
140 NSMutableDictionary* query = [@{
141 (id)kSecClass : (id)kSecClassGenericPassword,
142 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
143 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
144 (id)kSecAttrAccount : @"testaccount",
145 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
146 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
149 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
151 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
152 XCTAssertTrue(didSync, "Item synced properly");
153 XCTAssertNil((__bridge NSError*)error, "No error syncing item");
155 [blockExpectation fulfill];
156 }), @"_SecItemAddAndNotifyOnSync succeeded");
158 [self waitForExpectationsWithTimeout:5.0 handler:nil];
161 - (void)testAddAndNotifyOnSyncFailure {
162 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
164 [self startCKKSSubsystem];
165 [self.keychainView waitForFetchAndIncomingQueueProcessing];
167 // Due to item UUID selection, this item will be added with UUID DD7C2F9B-B22D-3B90-C299-E3B48174BFA3.
168 // Add it to CloudKit first!
169 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3"];
170 [self.keychainZone addToZone: ckr];
174 [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID];
176 NSMutableDictionary* query = [@{
177 (id)kSecClass : (id)kSecClassGenericPassword,
178 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
179 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
180 (id)kSecAttrAccount : @"testaccount",
181 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
182 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
185 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
187 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
188 XCTAssertFalse(didSync, "Item did not sync (as expected)");
189 XCTAssertNotNil((__bridge NSError*)error, "error exists when item fails to sync");
191 [blockExpectation fulfill];
192 }), @"_SecItemAddAndNotifyOnSync succeeded");
194 [self waitForExpectationsWithTimeout:5.0 handler:nil];
195 [self waitForCKModifications];
198 - (void)testAddAndNotifyOnSyncLoggedOut {
199 // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations.
200 self.accountStatus = CKAccountStatusNoAccount;
201 self.silentFetchesAllowed = false;
202 [self startCKKSSubsystem];
204 NSMutableDictionary* query = [@{
205 (id)kSecClass : (id)kSecClassGenericPassword,
206 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
207 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
208 (id)kSecAttrAccount : @"testaccount",
209 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
210 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
213 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
215 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
216 XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)");
217 XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out");
219 [blockExpectation fulfill];
220 }), @"_SecItemAddAndNotifyOnSync succeeded");
222 [self waitForExpectationsWithTimeout:5.0 handler:nil];
225 - (void)testAddAndNotifyOnSyncBeforeKeyHierarchyReady {
226 // Test starts with a key hierarchy in cloudkit and the TLK having arrived
227 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
228 [self saveTLKMaterialToKeychain:self.keychainZoneID];
230 // But block CloudKit fetches (so the key hierarchy won't be ready when we add this new item)
231 [self holdCloudKitFetches];
233 [self startCKKSSubsystem];
234 [self.keychainView.viewSetupOperation waitUntilFinished];
236 NSMutableDictionary* query = [@{
237 (id)kSecClass : (id)kSecClassGenericPassword,
238 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
239 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
240 (id)kSecAttrAccount : @"testaccount",
241 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
242 (id)kSecAttrSyncViewHint : self.keychainView.zoneName,
243 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
246 XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
248 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
249 XCTAssertTrue(didSync, "Item synced");
250 XCTAssertNil((__bridge NSError*)error, "Shouldn't have received an error syncing item");
252 [blockExpectation fulfill];
253 }), @"_SecItemAddAndNotifyOnSync succeeded");
255 // We should be in the 'initialized' state, but no further
256 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateInitialized] wait:100*NSEC_PER_MSEC], @"Should have reached key state 'initialized', but no further");
258 // When we release the fetch, the callback should still fire and the item should upload
259 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
260 [self releaseCloudKitFetchHold];
261 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:5*NSEC_PER_SEC], @"Should have reached key state 'ready'");
263 // Verify that the item was written to CloudKit
264 OCMVerifyAllWithDelay(self.mockDatabase, 8);
266 [self waitForExpectationsWithTimeout:5.0 handler:nil];
269 - (BOOL (^) (CKRecord*)) checkPCSFieldsBlock: (CKRecordZoneID*) zoneID
270 PCSServiceIdentifier:(NSNumber*)servIdentifier
271 PCSPublicKey:(NSData*)publicKey
272 PCSPublicIdentity:(NSData*)publicIdentity
274 __weak __typeof(self) weakSelf = self;
275 return ^BOOL(CKRecord* record) {
276 __strong __typeof(weakSelf) strongSelf = weakSelf;
277 XCTAssertNotNil(strongSelf, "self exists");
279 XCTAssert([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier], "PCS Service identifier matches input");
280 XCTAssert([record[SecCKRecordPCSPublicKey] isEqual: publicKey], "PCS Public Key matches input");
281 XCTAssert([record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity], "PCS Public Identity matches input");
283 if([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier] &&
284 [record[SecCKRecordPCSPublicKey] isEqual: publicKey] &&
285 [record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity]) {
293 - (void)testPCSUnencryptedFieldsAdd {
295 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
297 [self startCKKSSubsystem];
298 [self.keychainView waitUntilAllOperationsAreFinished];
300 NSNumber* servIdentifier = @3;
301 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
302 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
304 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
305 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
306 PCSServiceIdentifier:(NSNumber *)servIdentifier
307 PCSPublicKey:publicKey
308 PCSPublicIdentity:publicIdentity]];
310 NSMutableDictionary* query = [@{
311 (id)kSecClass : (id)kSecClassGenericPassword,
312 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
313 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
314 (id)kSecAttrAccount : @"testaccount",
315 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
316 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
317 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
318 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
319 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
320 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
323 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
325 // Verify that the item is written to CloudKit
326 OCMVerifyAllWithDelay(self.mockDatabase, 4);
328 CFTypeRef item = NULL;
329 query[(id)kSecValueData] = nil;
330 query[(id)kSecReturnAttributes] = @YES;
331 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
333 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
334 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Service Identifier exists");
335 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key exists");
336 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity exists");
338 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
339 // the record ID is likely DD7C2F9B-B22D-3B90-C299-E3B48174BFA3
340 [self waitForCKModifications];
341 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
342 CKRecord* record = self.keychainZone.currentDatabase[recordID];
343 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
345 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], servIdentifier, "Service identifier sent to cloudkit");
346 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], publicKey, "public key sent to cloudkit");
347 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], publicIdentity, "public identity sent to cloudkit");
350 - (void)testPCSUnencryptedFieldsModify {
351 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
353 [self startCKKSSubsystem];
354 [self.keychainView waitUntilAllOperationsAreFinished];
356 NSNumber* servIdentifier = @3;
357 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
358 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
360 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
361 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
362 PCSServiceIdentifier:(NSNumber *)servIdentifier
363 PCSPublicKey:publicKey
364 PCSPublicIdentity:publicIdentity]];
366 NSMutableDictionary* query = [@{
367 (id)kSecClass : (id)kSecClassGenericPassword,
368 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
369 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
370 (id)kSecAttrAccount : @"testaccount",
371 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
372 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
373 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
374 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
375 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
376 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
379 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
381 OCMVerifyAllWithDelay(self.mockDatabase, 8);
382 [self waitForCKModifications];
384 query[(id)kSecValueData] = nil;
385 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
386 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
387 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
390 publicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
392 NSNumber* newServiceIdentifier = @10;
393 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
394 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
396 NSDictionary* update = @{
397 (id)kSecAttrPCSPlaintextServiceIdentifier : newServiceIdentifier,
398 (id)kSecAttrPCSPlaintextPublicKey : newPublicKey,
399 (id)kSecAttrPCSPlaintextPublicIdentity : newPublicIdentity,
402 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
403 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
404 PCSServiceIdentifier:(NSNumber *)newServiceIdentifier
405 PCSPublicKey:newPublicKey
406 PCSPublicIdentity:newPublicIdentity]];
408 XCTAssertEqual(errSecSuccess, SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update), @"SecItemUpdate succeeded");
409 OCMVerifyAllWithDelay(self.mockDatabase, 8);
411 CFTypeRef item = NULL;
412 query[(id)kSecValueData] = nil;
413 query[(id)kSecReturnAttributes] = @YES;
414 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
416 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
417 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], newServiceIdentifier, "Service Identifier exists");
418 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], newPublicKey, "public key exists");
419 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], newPublicIdentity, "public identity exists");
421 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
422 // the record ID is likely DD7C2F9B-B22D-3B90-C299-E3B48174BFA3
423 [self waitForCKModifications];
424 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
425 CKRecord* record = self.keychainZone.currentDatabase[recordID];
426 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
428 XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], newServiceIdentifier, "Service identifier sent to cloudkit");
429 XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], newPublicKey, "public key sent to cloudkit");
430 XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], newPublicIdentity, "public identity sent to cloudkit");
433 // As of [<rdar://problem/32558310> CKKS: Re-authenticate PCSPublicFields], these fields are NOT server-modifiable. This test proves it.
434 - (void)testPCSUnencryptedFieldsServerModifyFail {
435 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
437 [self startCKKSSubsystem];
438 [self.keychainView waitForKeyHierarchyReadiness];
440 NSNumber* servIdentifier = @3;
441 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
442 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
444 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
445 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
446 PCSServiceIdentifier:(NSNumber *)servIdentifier
447 PCSPublicKey:publicKey
448 PCSPublicIdentity:publicIdentity]];
450 NSMutableDictionary* query = [@{
451 (id)kSecClass : (id)kSecClassGenericPassword,
452 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
453 (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
454 (id)kSecAttrAccount : @"testaccount",
455 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
456 (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
457 (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue,
458 (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
459 (id)kSecAttrPCSPlaintextPublicKey : publicKey,
460 (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
463 XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
465 OCMVerifyAllWithDelay(self.mockDatabase, 4);
466 [self waitForCKModifications];
468 // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
469 // the record ID is likely DD7C2F9B-B22D-3B90-C299-E3B48174BFA3
470 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
471 CKRecord* record = self.keychainZone.currentDatabase[recordID];
472 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
474 // Items are encrypted using encv2
475 XCTAssertEqualObjects(record[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
478 // Test has already failed; find the record just to be nice.
479 for(CKRecord* maybe in self.keychainZone.currentDatabase.allValues) {
480 if(maybe[SecCKRecordPCSServiceIdentifier] != nil) {
486 NSNumber* newServiceIdentifier = @10;
487 NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding];
488 NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding];
490 // Change the public key and public identity
491 record = [record copyWithZone: nil];
492 record[SecCKRecordPCSServiceIdentifier] = newServiceIdentifier;
493 record[SecCKRecordPCSPublicKey] = newPublicKey;
494 record[SecCKRecordPCSPublicIdentity] = newPublicIdentity;
495 [self.keychainZone addToZone: record];
497 // Trigger a notification
498 [self.keychainView notifyZoneChange:nil];
499 [self.keychainView waitForFetchAndIncomingQueueProcessing];
501 CFTypeRef item = NULL;
502 query[(id)kSecValueData] = nil;
503 query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
504 query[(id)kSecAttrPCSPlaintextPublicKey] = nil;
505 query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
506 query[(id)kSecReturnAttributes] = @YES;
507 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
509 NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item);
510 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "service identifier is not updated");
511 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key not updated");
512 XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity not updated");
515 -(void)testPCSUnencryptedFieldsRecieveUnauthenticatedFields {
516 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
518 [self startCKKSSubsystem];
519 [self.keychainView waitForKeyHierarchyReadiness];
521 NSNumber* servIdentifier = @3;
522 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
523 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
525 NSError* error = nil;
527 // Manually encrypt an item
528 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
529 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
530 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
531 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
532 parentKeyUUID:self.keychainZoneKeys.classC.uuid
533 zoneID:recordID.zoneID];
534 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
535 XCTAssertNotNil(itemkey, "Got a key");
536 cipheritem.wrappedkey = itemkey.wrappedkey;
537 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
539 cipheritem.encver = CKKSItemEncryptionVersion1;
541 // This item has the PCS public fields, but they are not authenticated
542 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
543 cipheritem.plaintextPCSPublicKey = publicKey;
544 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
546 NSDictionary<NSString*, NSData*>* authenticatedData = [cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion1];
547 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
548 XCTAssertNil(error, "no error encrypting object");
549 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
551 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
553 [self.keychainView waitForFetchAndIncomingQueueProcessing];
555 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
556 (id)kSecReturnAttributes: @YES,
557 (id)kSecAttrSynchronizable: @YES,
558 (id)kSecAttrAccount: @"account-delete-me",
559 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
561 CFTypeRef cfresult = NULL;
562 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
564 NSDictionary* result = CFBridgingRelease(cfresult);
565 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
566 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
567 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
570 -(void)testPCSUnencryptedFieldsRecieveAuthenticatedFields {
571 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
573 [self startCKKSSubsystem];
574 [self.keychainView waitForKeyHierarchyReadiness];
576 NSNumber* servIdentifier = @3;
577 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
578 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
580 NSError* error = nil;
582 // Manually encrypt an item
583 NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85";
584 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID];
585 NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID];
586 CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName
587 parentKeyUUID:self.keychainZoneKeys.classC.uuid
588 zoneID:recordID.zoneID];
589 CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error];
590 XCTAssertNotNil(itemkey, "Got a key");
591 cipheritem.wrappedkey = itemkey.wrappedkey;
592 XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key");
594 cipheritem.encver = CKKSItemEncryptionVersion2;
596 // This item has the PCS public fields, and they are authenticated (since we're using v2)
597 cipheritem.plaintextPCSServiceIdentifier = servIdentifier;
598 cipheritem.plaintextPCSPublicKey = publicKey;
599 cipheritem.plaintextPCSPublicIdentity = publicIdentity;
601 // Use version 2, so PCS plaintext fields will be authenticated
602 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy];
604 cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error];
605 XCTAssertNil(error, "no error encrypting object");
606 XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext");
608 [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]];
610 [self.keychainView waitForFetchAndIncomingQueueProcessing];
612 NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword,
613 (id)kSecReturnAttributes: @YES,
614 (id)kSecAttrSynchronizable: @YES,
615 (id)kSecAttrAccount: @"account-delete-me",
616 (id)kSecMatchLimit: (id)kSecMatchLimitOne,
618 CFTypeRef cfresult = NULL;
619 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item");
621 NSDictionary* result = CFBridgingRelease(cfresult);
622 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier");
623 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key");
624 XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity");
626 // Test that if this item is updated, it remains encrypted in v2
627 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
628 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
629 PCSServiceIdentifier:(NSNumber *)servIdentifier
630 PCSPublicKey:publicKey
631 PCSPublicIdentity:publicIdentity]];
632 [self updateGenericPassword:@"different password" account:@"account-delete-me"];
634 OCMVerifyAllWithDelay(self.mockDatabase, 4);
635 [self waitForCKModifications];
637 CKRecord* newRecord = self.keychainZone.currentDatabase[recordID];
638 XCTAssertEqualObjects(newRecord[SecCKRecordPCSServiceIdentifier], servIdentifier, "Didn't change service identifier");
639 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicKey], publicKey, "Didn't change public key");
640 XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicIdentity], publicIdentity, "Didn't change public identity");
641 XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2");
644 -(void)testResetLocal {
645 // Test starts with nothing in database, but one in our fake CloudKit.
646 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
647 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
649 // Spin up CKKS subsystem.
650 [self startCKKSSubsystem];
652 // We expect a single record to be uploaded
653 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
654 [self addGenericPassword: @"data" account: @"account-delete-me"];
655 OCMVerifyAllWithDelay(self.mockDatabase, 8);
657 // After the local reset, we expect: a fetch, then nothing
658 self.silentFetchesAllowed = false;
659 [self expectCKFetch];
661 dispatch_semaphore_t resetSemaphore = dispatch_semaphore_create(0);
662 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
663 XCTAssertNil(result, "no error resetting local");
664 secnotice("ckks", "Received a rpcResetLocal callback");
665 dispatch_semaphore_signal(resetSemaphore);
668 XCTAssertEqual(0, dispatch_semaphore_wait(resetSemaphore, 4*NSEC_PER_SEC), "Semaphore wait did not time out");
670 OCMVerifyAllWithDelay(self.mockDatabase, 8);
672 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
673 [self addGenericPassword:@"asdf"
674 account:@"account-class-A"
676 access:(id)kSecAttrAccessibleWhenUnlocked
677 expecting:errSecSuccess
678 message:@"Adding class A item"];
679 OCMVerifyAllWithDelay(self.mockDatabase, 8);
682 -(void)testResetLocalWhileLoggedOut {
683 // We're "logged in to" cloudkit but not in circle.
684 self.circleStatus = kSOSCCNotInCircle;
685 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
686 self.silentFetchesAllowed = false;
688 // Test starts with local TLK and key hierarhcy in our fake cloudkit
689 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
690 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
692 // Spin up CKKS subsystem.
693 [self startCKKSSubsystem];
695 NSData* changeTokenData = [[[NSUUID UUID] UUIDString] dataUsingEncoding:NSUTF8StringEncoding];
696 CKServerChangeToken* changeToken = [[CKServerChangeToken alloc] initWithData:changeTokenData];
697 [self.keychainView dispatchSync: ^bool{
698 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
699 ckse.changeToken = changeToken;
701 NSError* error = nil;
702 [ckse saveToDatabase:&error];
703 XCTAssertNil(error, "No error saving new zone state to database");
706 dispatch_semaphore_t resetSemaphore = dispatch_semaphore_create(0);
707 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
708 XCTAssertNil(result, "no error resetting cloudkit");
709 secnotice("ckks", "Received a rpcResetLocal callback");
710 dispatch_semaphore_signal(resetSemaphore);
713 XCTAssertEqual(0, dispatch_semaphore_wait(resetSemaphore, 400*NSEC_PER_SEC), "Semaphore wait did not time out");
715 [self.keychainView dispatchSync: ^bool{
716 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName];
717 XCTAssertNotEqualObjects(changeToken, ckse.changeToken, "Change token is reset");
720 // Now log in, and see what happens! It should re-fetch, pick up the old key hierarchy, and use it
721 self.silentFetchesAllowed = true;
722 self.circleStatus = kSOSCCInCircle;
723 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
725 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
726 [self addGenericPassword:@"asdf"
727 account:@"account-class-A"
729 access:(id)kSecAttrAccessibleWhenUnlocked
730 expecting:errSecSuccess
731 message:@"Adding class A item"];
732 OCMVerifyAllWithDelay(self.mockDatabase, 8);
735 -(void)testResetCloudKitZone {
736 // Test starts with nothing in database, but one in our fake CloudKit.
737 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
738 [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
740 // Spin up CKKS subsystem.
741 [self startCKKSSubsystem];
743 // We expect a single record to be uploaded
744 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
745 [self addGenericPassword: @"data" account: @"account-delete-me"];
746 OCMVerifyAllWithDelay(self.mockDatabase, 8);
748 // We expect a key hierarchy upload, and then the class C item upload
749 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
750 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
752 dispatch_semaphore_t resetSemaphore = dispatch_semaphore_create(0);
753 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
754 XCTAssertNil(result, "no error resetting cloudkit");
755 secnotice("ckks", "Received a resetCloudKit callback");
756 dispatch_semaphore_signal(resetSemaphore);
759 XCTAssertEqual(0, dispatch_semaphore_wait(resetSemaphore, 4*NSEC_PER_SEC), "Semaphore wait did not time out");
761 OCMVerifyAllWithDelay(self.mockDatabase, 8);
763 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
764 [self addGenericPassword:@"asdf"
765 account:@"account-class-A"
767 access:(id)kSecAttrAccessibleWhenUnlocked
768 expecting:errSecSuccess
769 message:@"Adding class A item"];
770 OCMVerifyAllWithDelay(self.mockDatabase, 8);
773 - (void)testResetCloudKitZoneDuringWaitForTLK {
774 // Test starts with nothing in database, but one in our fake CloudKit.
776 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
778 // Spin up CKKS subsystem.
779 [self startCKKSSubsystem];
781 // No records should be uploaded
782 [self addGenericPassword: @"data" account: @"account-delete-me"];
784 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS should have entered waitfortlk");
786 // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going)
787 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
788 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
790 CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil];
791 XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold");
793 // Now, reset everything. The outgoingOp should get cancelled.
794 // We expect a key hierarchy upload, and then the class C item upload
795 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords:3 zoneID:self.keychainZoneID];
796 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
797 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
799 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
800 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
801 XCTAssertNil(result, "no error resetting cloudkit");
802 [resetExpectation fulfill];
804 [self waitForExpectations:@[resetExpectation] timeout:8.0];
806 XCTAssertTrue([outgoingOp isCancelled], "old stuck ProcessOutgoingQueue should be cancelled");
807 OCMVerifyAllWithDelay(self.mockDatabase, 8);
809 // And adding another item works too
810 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
811 [self addGenericPassword:@"asdf"
812 account:@"account-class-A"
814 access:(id)kSecAttrAccessibleWhenUnlocked
815 expecting:errSecSuccess
816 message:@"Adding class A item"];
817 OCMVerifyAllWithDelay(self.mockDatabase, 8);
821 * This test doesn't work, since the resetLocal fails. CKKS gets back into waitfortlk
822 * but that isn't considered a successful resetLocal.
824 - (void)testResetLocalDuringWaitForTLK {
825 // Test starts with nothing in database, but one in our fake CloudKit.
827 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
829 // Spin up CKKS subsystem.
830 [self startCKKSSubsystem];
832 // No records should be uploaded
833 [self addGenericPassword: @"data" account: @"account-delete-me"];
835 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS should have entered waitfortlk");
837 // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going)
838 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
839 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
841 CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil];
842 XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold");
844 // Now, reset everything. The outgoingOp should get cancelled.
845 // We expect a key hierarchy upload, and then the class C item upload
846 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords:3 zoneID:self.keychainZoneID];
847 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
848 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
850 XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
851 [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
852 XCTAssertNil(result, "no error resetting local");
853 [resetExpectation fulfill];
855 [self waitForExpectations:@[resetExpectation] timeout:8.0];
857 XCTAssertTrue([outgoingOp isCancelled], "old stuck ProcessOutgoingQueue should be cancelled");
858 OCMVerifyAllWithDelay(self.mockDatabase, 8);
860 // And adding another item works too
861 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
862 [self addGenericPassword:@"asdf"
863 account:@"account-class-A"
865 access:(id)kSecAttrAccessibleWhenUnlocked
866 expecting:errSecSuccess
867 message:@"Adding class A item"];
868 OCMVerifyAllWithDelay(self.mockDatabase, 8);
871 -(void)testResetCloudKitZoneWhileLoggedOut {
872 // We're "logged in to" cloudkit but not in circle.
873 self.circleStatus = kSOSCCNotInCircle;
874 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
875 self.silentFetchesAllowed = false;
877 // Test starts with nothing in database, but one in our fake CloudKit.
878 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
880 // Spin up CKKS subsystem.
881 [self startCKKSSubsystem];
883 [self.keychainView.viewSetupOperation waitUntilFinished];
884 // Reset setup, since that's the most likely state to be in (33866282)
885 [self.keychainView resetSetup];
887 CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
888 [self.keychainZone addToZone: ckr];
890 XCTAssertNotNil(self.keychainZone.currentDatabase, "Zone exists");
891 XCTAssertNotNil(self.keychainZone.currentDatabase[ckr.recordID], "An item exists in the fake zone");
893 dispatch_semaphore_t resetSemaphore = dispatch_semaphore_create(0);
894 [self.injectedManager rpcResetCloudKit:nil reply:^(NSError* result) {
895 XCTAssertNil(result, "no error resetting cloudkit");
896 secnotice("ckks", "Received a resetCloudKit callback");
897 dispatch_semaphore_signal(resetSemaphore);
900 XCTAssertEqual(0, dispatch_semaphore_wait(resetSemaphore, 400*NSEC_PER_SEC), "Semaphore wait did not time out");
902 XCTAssertNil(self.keychainZone.currentDatabase, "No zone anymore!");
903 OCMVerifyAllWithDelay(self.mockDatabase, 8);
905 // Now log in, and see what happens! It should create the zone again and upload a whole new key hierarchy
906 self.silentFetchesAllowed = true;
907 self.circleStatus = kSOSCCInCircle;
908 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
910 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
911 OCMVerifyAllWithDelay(self.mockDatabase, 8);
913 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
914 [self addGenericPassword:@"asdf"
915 account:@"account-class-A"
917 access:(id)kSecAttrAccessibleWhenUnlocked
918 expecting:errSecSuccess
919 message:@"Adding class A item"];
920 OCMVerifyAllWithDelay(self.mockDatabase, 8);
923 - (void)testRPCTLKMissingWhenMissing {
924 // Bring CKKS up in waitfortlk
925 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
926 [self startCKKSSubsystem];
928 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
930 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
932 [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) {
933 XCTAssertTrue(missing, "TLKs should be missing");
934 [callbackOccurs fulfill];
937 [self waitForExpectations:@[callbackOccurs] timeout:5.0];
940 - (void)testRPCTLKMissingWhenFound {
941 // Bring CKKS up in waitfortlk
942 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
943 [self saveTLKMaterialToKeychain:self.keychainZoneID];
944 [self startCKKSSubsystem];
946 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready''");
948 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
950 [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) {
951 XCTAssertFalse(missing, "TLKs should not be missing");
952 [callbackOccurs fulfill];
955 [self waitForExpectations:@[callbackOccurs] timeout:5.0];