/* * Copyright (c) 2016 Apple Inc. All Rights Reserved. * * @APPLE_LICENSE_HEADER_START@ * * This file contains Original Code and/or Modifications of Original Code * as defined in and that are subject to the Apple Public Source License * Version 2.0 (the 'License'). You may not use this file except in * compliance with the License. Please obtain a copy of the License at * http://www.opensource.apple.com/apsl/ and read it before using this * file. * * The Original Code and all software distributed under the License are * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. * Please see the License for the specific language governing rights and * limitations under the License. * * @APPLE_LICENSE_HEADER_END@ */ #if OCTAGON #import <CloudKit/CloudKit.h> #import <XCTest/XCTest.h> #import <OCMock/OCMock.h> #include <Security/SecItemPriv.h> #include <Security/SecEntitlements.h> #include <ipc/server_security_helpers.h> #import <Foundation/NSXPCConnection_Private.h> #import "keychain/categories/NSError+UsefulConstructors.h" #import "keychain/ckks/tests/CloudKitMockXCTest.h" #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h" #import "keychain/ckks/CKKS.h" #import "keychain/ckks/CKKSItem.h" #import "keychain/ckks/CKKSItemEncrypter.h" #import "keychain/ckks/CKKSKey.h" #import "keychain/ckks/CKKSViewManager.h" #import "keychain/ckks/CKKSZoneStateEntry.h" #import "keychain/ckks/CKKSControl.h" #import "keychain/ckks/CloudKitCategories.h" #import "keychain/ckks/tests/MockCloudKit.h" #import "keychain/ckks/tests/CKKSTests.h" #import "keychain/ckks/tests/CKKSTests+API.h" @implementation CloudKitKeychainSyncingTestsBase (APITests) -(NSMutableDictionary*)pcsAddItemQuery:(NSString*)account data:(NSData*)data serviceIdentifier:(NSNumber*)serviceIdentifier publicKey:(NSData*)publicKey publicIdentity:(NSData*)publicIdentity { return [@{ (id)kSecClass : (id)kSecClassGenericPassword, (id)kSecReturnPersistentRef: @YES, (id)kSecReturnAttributes: @YES, (id)kSecAttrAccessGroup : @"com.apple.security.ckks", (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock, (id)kSecAttrAccount : account, (id)kSecAttrSynchronizable : (id)kCFBooleanTrue, (id)kSecValueData : data, (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue, (id)kSecAttrPCSPlaintextServiceIdentifier : serviceIdentifier, (id)kSecAttrPCSPlaintextPublicKey : publicKey, (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity, } mutableCopy]; } -(NSDictionary*)pcsAddItem:(NSString*)account data:(NSData*)data serviceIdentifier:(NSNumber*)serviceIdentifier publicKey:(NSData*)publicKey publicIdentity:(NSData*)publicIdentity expectingSync:(bool)expectingSync { NSMutableDictionary* query = [self pcsAddItemQuery:account data:data serviceIdentifier:(NSNumber*)serviceIdentifier publicKey:(NSData*)publicKey publicIdentity:(NSData*)publicIdentity]; CFTypeRef result = NULL; XCTestExpectation* syncExpectation = [self expectationWithDescription: @"callback occurs"]; XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, &result, ^(bool didSync, CFErrorRef error) { if(expectingSync) { XCTAssertTrue(didSync, "Item synced"); XCTAssertNil((__bridge NSError*)error, "No error syncing item"); } else { XCTAssertFalse(didSync, "Item did not sync"); XCTAssertNotNil((__bridge NSError*)error, "Error syncing item"); } [syncExpectation fulfill]; }), @"_SecItemAddAndNotifyOnSync succeeded"); // Verify that the item was written to CloudKit OCMVerifyAllWithDelay(self.mockDatabase, 20); // In real code, you'd need to wait for the _SecItemAddAndNotifyOnSync callback to succeed before proceeding [self waitForExpectations:@[syncExpectation] timeout:20]; return (NSDictionary*) CFBridgingRelease(result); } - (BOOL (^) (CKRecord*)) checkPCSFieldsBlock: (CKRecordZoneID*) zoneID PCSServiceIdentifier:(NSNumber*)servIdentifier PCSPublicKey:(NSData*)publicKey PCSPublicIdentity:(NSData*)publicIdentity { __weak __typeof(self) weakSelf = self; return ^BOOL(CKRecord* record) { __strong __typeof(weakSelf) strongSelf = weakSelf; XCTAssertNotNil(strongSelf, "self exists"); XCTAssert([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier], "PCS Service identifier matches input"); XCTAssert([record[SecCKRecordPCSPublicKey] isEqual: publicKey], "PCS Public Key matches input"); XCTAssert([record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity], "PCS Public Identity matches input"); if([record[SecCKRecordPCSServiceIdentifier] isEqual: servIdentifier] && [record[SecCKRecordPCSPublicKey] isEqual: publicKey] && [record[SecCKRecordPCSPublicIdentity] isEqual: publicIdentity]) { return YES; } else { return NO; } }; } @end @interface CloudKitKeychainSyncingAPITests : CloudKitKeychainSyncingTestsBase @end @implementation CloudKitKeychainSyncingAPITests - (void)testSecuritydClientBringup { #if 0 CFErrorRef cferror = nil; xpc_endpoint_t endpoint = SecCreateSecuritydXPCServerEndpoint(&cferror); XCTAssertNil((__bridge id)cferror, "No error creating securityd endpoint"); XCTAssertNotNil(endpoint, "Received securityd endpoint"); #endif NSXPCInterface *interface = [NSXPCInterface interfaceWithProtocol:@protocol(SecuritydXPCProtocol)]; [SecuritydXPCClient configureSecuritydXPCProtocol: interface]; XCTAssertNotNil(interface, "Received a configured CKKS interface"); #if 0 NSXPCListenerEndpoint *listenerEndpoint = [[NSXPCListenerEndpoint alloc] init]; [listenerEndpoint _setEndpoint:endpoint]; NSXPCConnection* connection = [[NSXPCConnection alloc] initWithListenerEndpoint:listenerEndpoint]; XCTAssertNotNil(connection , "Received an active connection"); connection.remoteObjectInterface = interface; #endif } - (void)testAddAndNotifyOnSync { [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test. [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID]; [self startCKKSSubsystem]; // Let things shake themselves out. [self.keychainView waitForKeyHierarchyReadiness]; [self waitForCKModifications]; NSMutableDictionary* query = [@{ (id)kSecClass : (id)kSecClassGenericPassword, (id)kSecAttrAccessGroup : @"com.apple.security.ckks", (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock, (id)kSecAttrAccount : @"testaccount", (id)kSecAttrSynchronizable : (id)kCFBooleanTrue, (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding], } mutableCopy]; XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"]; XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) { XCTAssertTrue(didSync, "Item synced properly"); XCTAssertNil((__bridge NSError*)error, "No error syncing item"); [blockExpectation fulfill]; }), @"_SecItemAddAndNotifyOnSync succeeded"); [self waitForExpectationsWithTimeout:5.0 handler:nil]; } - (void)testAddAndNotifyOnSyncFailure { [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test. [self startCKKSSubsystem]; [self.keychainView waitForFetchAndIncomingQueueProcessing]; // Due to item UUID selection, this item will be added with UUID 50184A35-4480-E8BA-769B-567CF72F1EC0. // Add it to CloudKit first! CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"50184A35-4480-E8BA-769B-567CF72F1EC0"]; [self.keychainZone addToZone: ckr]; // Go for it! [self expectCKAtomicModifyItemRecordsUpdateFailure: self.keychainZoneID]; NSMutableDictionary* query = [@{ (id)kSecClass : (id)kSecClassGenericPassword, (id)kSecAttrAccessGroup : @"com.apple.security.ckks", (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock, (id)kSecAttrAccount : @"testaccount", (id)kSecAttrSynchronizable : (id)kCFBooleanTrue, (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding], (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // @ fake view hint for fake view } mutableCopy]; XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"]; XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) { XCTAssertFalse(didSync, "Item did not sync (as expected)"); XCTAssertNotNil((__bridge NSError*)error, "error exists when item fails to sync"); [blockExpectation fulfill]; }), @"_SecItemAddAndNotifyOnSync succeeded"); [self waitForExpectationsWithTimeout:5.0 handler:nil]; [self waitForCKModifications]; } - (void)testAddAndNotifyOnSyncLoggedOut { // Test starts with nothing in database and the user logged out of CloudKit. We expect no CKKS operations. self.accountStatus = CKAccountStatusNoAccount; self.silentFetchesAllowed = false; [self startCKKSSubsystem]; XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "CKKS should positively log out"); NSMutableDictionary* query = [@{ (id)kSecClass : (id)kSecClassGenericPassword, (id)kSecAttrAccessGroup : @"com.apple.security.ckks", (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock, (id)kSecAttrAccount : @"testaccount", (id)kSecAttrSynchronizable : (id)kCFBooleanTrue, (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding], } mutableCopy]; XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"]; XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) { XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)"); XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out"); [blockExpectation fulfill]; }), @"_SecItemAddAndNotifyOnSync succeeded"); [self waitForExpectationsWithTimeout:5.0 handler:nil]; } - (void)testAddAndNotifyOnSyncAccountStatusUnclear { // Test starts with nothing in database, but CKKS hasn't been told we've logged out yet. // We expect no CKKS operations. self.accountStatus = CKAccountStatusNoAccount; self.silentFetchesAllowed = false; NSMutableDictionary* query = [@{ (id)kSecClass : (id)kSecClassGenericPassword, (id)kSecAttrAccessGroup : @"com.apple.security.ckks", (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock, (id)kSecAttrAccount : @"testaccount", (id)kSecAttrSynchronizable : (id)kCFBooleanTrue, (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding], } mutableCopy]; XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"]; XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) { XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)"); XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out"); [blockExpectation fulfill]; }), @"_SecItemAddAndNotifyOnSync succeeded"); // And now, allow CKKS to discover we're logged out [self startCKKSSubsystem]; XCTAssertEqual(0, [self.keychainView.loggedOut wait:20*NSEC_PER_SEC], "CKKS should positively log out"); [self waitForExpectationsWithTimeout:5.0 handler:nil]; } - (void)testAddAndNotifyOnSyncBeforeKeyHierarchyReady { // Test starts with a key hierarchy in cloudkit and the TLK having arrived [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID]; [self saveTLKMaterialToKeychain:self.keychainZoneID]; [self expectCKKSTLKSelfShareUpload:self.keychainZoneID]; // But block CloudKit fetches (so the key hierarchy won't be ready when we add this new item) [self holdCloudKitFetches]; [self startCKKSSubsystem]; XCTAssertEqual(0, [self.keychainView.loggedIn wait:20*NSEC_PER_SEC], "CKKS should log in"); [self.keychainView.zoneSetupOperation waitUntilFinished]; NSMutableDictionary* query = [@{ (id)kSecClass : (id)kSecClassGenericPassword, (id)kSecAttrAccessGroup : @"com.apple.security.ckks", (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock, (id)kSecAttrAccount : @"testaccount", (id)kSecAttrSynchronizable : (id)kCFBooleanTrue, (id)kSecAttrSyncViewHint : self.keychainView.zoneName, (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding], } mutableCopy]; XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"]; XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) { XCTAssertTrue(didSync, "Item synced"); XCTAssertNil((__bridge NSError*)error, "Shouldn't have received an error syncing item"); [blockExpectation fulfill]; }), @"_SecItemAddAndNotifyOnSync succeeded"); // We should be in the 'fetch' state, but no further XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateFetch] wait:20*NSEC_PER_SEC], @"Should have reached key state 'fetch', but no further"); // When we release the fetch, the callback should still fire and the item should upload [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID]; [self releaseCloudKitFetchHold]; XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Should have reached key state 'ready'"); // Verify that the item was written to CloudKit OCMVerifyAllWithDelay(self.mockDatabase, 20); [self waitForExpectationsWithTimeout:5.0 handler:nil]; } - (void)testPCSUnencryptedFieldsAdd { [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test. [self startCKKSSubsystem]; [self.keychainView waitForKeyHierarchyReadiness]; NSNumber* servIdentifier = @3; NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding]; NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding]; [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkPCSFieldsBlock:self.keychainZoneID PCSServiceIdentifier:(NSNumber *)servIdentifier PCSPublicKey:publicKey PCSPublicIdentity:publicIdentity]]; NSMutableDictionary* query = [@{ (id)kSecClass : (id)kSecClassGenericPassword, (id)kSecAttrAccessGroup : @"com.apple.security.ckks", (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock, (id)kSecAttrAccount : @"testaccount", (id)kSecAttrSynchronizable : (id)kCFBooleanTrue, (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding], (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue, (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier, (id)kSecAttrPCSPlaintextPublicKey : publicKey, (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity, (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // allows a CKKSScanOperation to find this item } mutableCopy]; XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded"); // Verify that the item is written to CloudKit OCMVerifyAllWithDelay(self.mockDatabase, 20); CFTypeRef item = NULL; query[(id)kSecValueData] = nil; query[(id)kSecReturnAttributes] = @YES; XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist"); NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item); XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Service Identifier exists"); XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key exists"); XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity exists"); // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes, // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0 [self waitForCKModifications]; CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID]; CKRecord* record = self.keychainZone.currentDatabase[recordID]; XCTAssertNotNil(record, "Found record in CloudKit at expected UUID"); XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], servIdentifier, "Service identifier sent to cloudkit"); XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], publicKey, "public key sent to cloudkit"); XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], publicIdentity, "public identity sent to cloudkit"); } - (void)testPCSUnencryptedFieldsModify { [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test. [self startCKKSSubsystem]; [self.keychainView waitForKeyHierarchyReadiness]; NSNumber* servIdentifier = @3; NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding]; NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding]; [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkPCSFieldsBlock:self.keychainZoneID PCSServiceIdentifier:(NSNumber *)servIdentifier PCSPublicKey:publicKey PCSPublicIdentity:publicIdentity]]; NSMutableDictionary* query = [@{ (id)kSecClass : (id)kSecClassGenericPassword, (id)kSecAttrAccessGroup : @"com.apple.security.ckks", (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock, (id)kSecAttrAccount : @"testaccount", (id)kSecAttrSynchronizable : (id)kCFBooleanTrue, (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding], (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue, (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier, (id)kSecAttrPCSPlaintextPublicKey : publicKey, (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity, (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // allows a CKKSScanOperation to find this item } mutableCopy]; XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded"); OCMVerifyAllWithDelay(self.mockDatabase, 20); [self waitForCKModifications]; query[(id)kSecValueData] = nil; query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil; query[(id)kSecAttrPCSPlaintextPublicKey] = nil; query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil; servIdentifier = @1; publicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding]; NSNumber* newServiceIdentifier = @10; NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding]; NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding]; NSDictionary* update = @{ (id)kSecAttrPCSPlaintextServiceIdentifier : newServiceIdentifier, (id)kSecAttrPCSPlaintextPublicKey : newPublicKey, (id)kSecAttrPCSPlaintextPublicIdentity : newPublicIdentity, }; [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkPCSFieldsBlock:self.keychainZoneID PCSServiceIdentifier:(NSNumber *)newServiceIdentifier PCSPublicKey:newPublicKey PCSPublicIdentity:newPublicIdentity]]; XCTAssertEqual(errSecSuccess, SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update), @"SecItemUpdate succeeded"); OCMVerifyAllWithDelay(self.mockDatabase, 20); CFTypeRef item = NULL; query[(id)kSecValueData] = nil; query[(id)kSecReturnAttributes] = @YES; XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist"); NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item); XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], newServiceIdentifier, "Service Identifier exists"); XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], newPublicKey, "public key exists"); XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], newPublicIdentity, "public identity exists"); // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes, // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0 [self waitForCKModifications]; CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID]; CKRecord* record = self.keychainZone.currentDatabase[recordID]; XCTAssertNotNil(record, "Found record in CloudKit at expected UUID"); XCTAssertEqualObjects(record[SecCKRecordPCSServiceIdentifier], newServiceIdentifier, "Service identifier sent to cloudkit"); XCTAssertEqualObjects(record[SecCKRecordPCSPublicKey], newPublicKey, "public key sent to cloudkit"); XCTAssertEqualObjects(record[SecCKRecordPCSPublicIdentity], newPublicIdentity, "public identity sent to cloudkit"); } // As of [<rdar://problem/32558310> CKKS: Re-authenticate PCSPublicFields], these fields are NOT server-modifiable. This test proves it. - (void)testPCSUnencryptedFieldsServerModifyFail { [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test. [self startCKKSSubsystem]; [self.keychainView waitForKeyHierarchyReadiness]; NSNumber* servIdentifier = @3; NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding]; NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding]; [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkPCSFieldsBlock:self.keychainZoneID PCSServiceIdentifier:(NSNumber *)servIdentifier PCSPublicKey:publicKey PCSPublicIdentity:publicIdentity]]; NSMutableDictionary* query = [@{ (id)kSecClass : (id)kSecClassGenericPassword, (id)kSecAttrAccessGroup : @"com.apple.security.ckks", (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock, (id)kSecAttrAccount : @"testaccount", (id)kSecAttrSynchronizable : (id)kCFBooleanTrue, (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding], (id)kSecAttrDeriveSyncIDFromItemAttributes : (id)kCFBooleanTrue, (id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier, (id)kSecAttrPCSPlaintextPublicKey : publicKey, (id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity, (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // fake, for CKKSScanOperation } mutableCopy]; XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded"); OCMVerifyAllWithDelay(self.mockDatabase, 20); [self waitForCKModifications]; // Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes, // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0 CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID]; CKRecord* record = self.keychainZone.currentDatabase[recordID]; XCTAssertNotNil(record, "Found record in CloudKit at expected UUID"); // Items are encrypted using encv2 XCTAssertEqualObjects(record[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2"); if(!record) { // Test has already failed; find the record just to be nice. for(CKRecord* maybe in self.keychainZone.currentDatabase.allValues) { if(maybe[SecCKRecordPCSServiceIdentifier] != nil) { record = maybe; } } } NSNumber* newServiceIdentifier = @10; NSData* newPublicKey = [@"new public key" dataUsingEncoding:NSUTF8StringEncoding]; NSData* newPublicIdentity = [@"new public identity" dataUsingEncoding:NSUTF8StringEncoding]; // Change the public key and public identity record = [record copyWithZone: nil]; record[SecCKRecordPCSServiceIdentifier] = newServiceIdentifier; record[SecCKRecordPCSPublicKey] = newPublicKey; record[SecCKRecordPCSPublicIdentity] = newPublicIdentity; [self.keychainZone addToZone: record]; // Trigger a notification [self.keychainView notifyZoneChange:nil]; [self.keychainView waitForFetchAndIncomingQueueProcessing]; CFTypeRef item = NULL; query[(id)kSecValueData] = nil; query[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil; query[(id)kSecAttrPCSPlaintextPublicKey] = nil; query[(id)kSecAttrPCSPlaintextPublicIdentity] = nil; query[(id)kSecReturnAttributes] = @YES; XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist"); NSDictionary* itemAttributes = (NSDictionary*) CFBridgingRelease(item); XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "service identifier is not updated"); XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "public key not updated"); XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity not updated"); } -(void)testPCSUnencryptedFieldsRecieveUnauthenticatedFields { [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test. [self startCKKSSubsystem]; [self.keychainView waitForKeyHierarchyReadiness]; NSNumber* servIdentifier = @3; NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding]; NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding]; NSError* error = nil; // Manually encrypt an item NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"; CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID]; NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID]; CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName parentKeyUUID:self.keychainZoneKeys.classC.uuid zoneID:recordID.zoneID]; CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error]; XCTAssertNotNil(itemkey, "Got a key"); cipheritem.wrappedkey = itemkey.wrappedkey; XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key"); cipheritem.encver = CKKSItemEncryptionVersion1; // This item has the PCS public fields, but they are not authenticated cipheritem.plaintextPCSServiceIdentifier = servIdentifier; cipheritem.plaintextPCSPublicKey = publicKey; cipheritem.plaintextPCSPublicIdentity = publicIdentity; NSDictionary<NSString*, NSData*>* authenticatedData = [cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion1]; cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error]; XCTAssertNil(error, "no error encrypting object"); XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext"); [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]]; [self.keychainView waitForFetchAndIncomingQueueProcessing]; NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword, (id)kSecReturnAttributes: @YES, (id)kSecAttrSynchronizable: @YES, (id)kSecAttrAccount: @"account-delete-me", (id)kSecMatchLimit: (id)kSecMatchLimitOne, }; CFTypeRef cfresult = NULL; XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item"); NSDictionary* result = CFBridgingRelease(cfresult); XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier"); XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key"); XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity"); } -(void)testPCSUnencryptedFieldsRecieveAuthenticatedFields { [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test. [self startCKKSSubsystem]; [self.keychainView waitForKeyHierarchyReadiness]; [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]]; NSNumber* servIdentifier = @3; NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding]; NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding]; NSError* error = nil; // Manually encrypt an item NSString* recordName = @"7B598D31-F9C5-481E-98AC-5A507ACB2D85"; CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName:recordName zoneID:self.keychainZoneID]; NSDictionary* item = [self fakeRecordDictionary: @"account-delete-me" zoneID:self.keychainZoneID]; CKKSItem* cipheritem = [[CKKSItem alloc] initWithUUID:recordID.recordName parentKeyUUID:self.keychainZoneKeys.classC.uuid zoneID:recordID.zoneID]; CKKSKey* itemkey = [CKKSKey randomKeyWrappedByParent: self.keychainZoneKeys.classC error:&error]; XCTAssertNotNil(itemkey, "Got a key"); cipheritem.wrappedkey = itemkey.wrappedkey; XCTAssertNotNil(cipheritem.wrappedkey, "Got a wrapped key"); cipheritem.encver = CKKSItemEncryptionVersion2; // This item has the PCS public fields, and they are authenticated (since we're using v2) cipheritem.plaintextPCSServiceIdentifier = servIdentifier; cipheritem.plaintextPCSPublicKey = publicKey; cipheritem.plaintextPCSPublicIdentity = publicIdentity; // Use version 2, so PCS plaintext fields will be authenticated NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[cipheritem makeAuthenticatedDataDictionaryUpdatingCKKSItem: nil encryptionVersion:CKKSItemEncryptionVersion2] mutableCopy]; cipheritem.encitem = [CKKSItemEncrypter encryptDictionary:item key:itemkey.aessivkey authenticatedData:authenticatedData error:&error]; XCTAssertNil(error, "no error encrypting object"); XCTAssertNotNil(cipheritem.encitem, "Recieved ciphertext"); [self.keychainZone addToZone:[cipheritem CKRecordWithZoneID: recordID.zoneID]]; [self.keychainView waitForFetchAndIncomingQueueProcessing]; NSDictionary* query = @{(id)kSecClass: (id)kSecClassGenericPassword, (id)kSecReturnAttributes: @YES, (id)kSecAttrSynchronizable: @YES, (id)kSecAttrAccount: @"account-delete-me", (id)kSecMatchLimit: (id)kSecMatchLimitOne, }; CFTypeRef cfresult = NULL; XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Found synced item"); NSDictionary* result = CFBridgingRelease(cfresult); XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextServiceIdentifier], servIdentifier, "Received PCS service identifier"); XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicKey], publicKey, "Received PCS public key"); XCTAssertEqualObjects(result[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "Received PCS public identity"); // Test that if this item is updated, it remains encrypted in v2 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkPCSFieldsBlock:self.keychainZoneID PCSServiceIdentifier:(NSNumber *)servIdentifier PCSPublicKey:publicKey PCSPublicIdentity:publicIdentity]]; [self updateGenericPassword:@"different password" account:@"account-delete-me"]; OCMVerifyAllWithDelay(self.mockDatabase, 20); [self waitForCKModifications]; CKRecord* newRecord = self.keychainZone.currentDatabase[recordID]; XCTAssertEqualObjects(newRecord[SecCKRecordPCSServiceIdentifier], servIdentifier, "Didn't change service identifier"); XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicKey], publicKey, "Didn't change public key"); XCTAssertEqualObjects(newRecord[SecCKRecordPCSPublicIdentity], publicIdentity, "Didn't change public identity"); XCTAssertEqualObjects(newRecord[SecCKRecordEncryptionVersionKey], [NSNumber numberWithInteger:(int) CKKSItemEncryptionVersion2], "Uploaded using encv2"); } -(void)testResetLocal { // Test starts with nothing in database, but one in our fake CloudKit. [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID]; [self expectCKKSTLKSelfShareUpload:self.keychainZoneID]; [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID]; // Spin up CKKS subsystem. [self startCKKSSubsystem]; // We expect a single record to be uploaded [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]]; [self addGenericPassword: @"data" account: @"account-delete-me"]; OCMVerifyAllWithDelay(self.mockDatabase, 20); // After the local reset, we expect: a fetch, then nothing self.silentFetchesAllowed = false; [self expectCKFetch]; XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"]; [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) { XCTAssertNil(result, "no error resetting local"); [resetExpectation fulfill]; }]; [self waitForExpectations:@[resetExpectation] timeout:20]; OCMVerifyAllWithDelay(self.mockDatabase, 20); [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]]; [self addGenericPassword:@"asdf" account:@"account-class-A" viewHint:nil access:(id)kSecAttrAccessibleWhenUnlocked expecting:errSecSuccess message:@"Adding class A item"]; OCMVerifyAllWithDelay(self.mockDatabase, 20); } -(void)testResetLocalWhileLoggedOut { // We're "logged in to" cloudkit but not in circle. self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCNotInCircle error:nil]; [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal]; self.silentFetchesAllowed = false; // Test starts with local TLK and key hierarchy in our fake cloudkit [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID]; [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID]; // Spin up CKKS subsystem. [self startCKKSSubsystem]; XCTAssertEqual(0, [self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], "Should have been told of a 'logout' event on startup"); NSData* changeTokenData = [[[NSUUID UUID] UUIDString] dataUsingEncoding:NSUTF8StringEncoding]; CKServerChangeToken* changeToken = [[CKServerChangeToken alloc] initWithData:changeTokenData]; [self.keychainView dispatchSync: ^bool{ CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName]; ckse.changeToken = changeToken; NSError* error = nil; [ckse saveToDatabase:&error]; XCTAssertNil(error, "No error saving new zone state to database"); return true; }]; XCTestExpectation* resetExpectation = [self expectationWithDescription: @"local reset callback occurs"]; [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) { XCTAssertNil(result, "no error resetting local"); secnotice("ckks", "Received a rpcResetLocal callback"); [resetExpectation fulfill]; }]; [self waitForExpectations:@[resetExpectation] timeout:20]; [self.keychainView dispatchSync: ^bool{ CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state:self.keychainView.zoneName]; XCTAssertNotEqualObjects(changeToken, ckse.changeToken, "Change token is reset"); return true; }]; // Now log in, and see what happens! It should re-fetch, pick up the old key hierarchy, and use it [self expectCKKSTLKSelfShareUpload:self.keychainZoneID]; self.silentFetchesAllowed = true; self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];; [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal]; [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]]; [self addGenericPassword:@"asdf" account:@"account-class-A" viewHint:nil access:(id)kSecAttrAccessibleWhenUnlocked expecting:errSecSuccess message:@"Adding class A item"]; OCMVerifyAllWithDelay(self.mockDatabase, 20); } -(void)testResetLocalMultipleTimes { // Test starts with nothing in database, but one in our fake CloudKit. [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID]; [self expectCKKSTLKSelfShareUpload:self.keychainZoneID]; [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID]; // Spin up CKKS subsystem. [self startCKKSSubsystem]; // We expect a single record to be uploaded XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'"); [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]]; [self addGenericPassword: @"data" account: @"account-delete-me"]; OCMVerifyAllWithDelay(self.mockDatabase, 20); [self waitForCKModifications]; // We're going to request a bunch of CloudKit resets, but hold them from finishing [self holdCloudKitFetches]; XCTestExpectation* resetExpectation0 = [self expectationWithDescription: @"reset callback(0) occurs"]; XCTestExpectation* resetExpectation1 = [self expectationWithDescription: @"reset callback(1) occurs"]; XCTestExpectation* resetExpectation2 = [self expectationWithDescription: @"reset callback(2) occurs"]; [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) { XCTAssertNil(result, "should receive no error resetting local"); secnotice("ckksreset", "Received a rpcResetLocal(0) callback"); [resetExpectation0 fulfill]; }]; [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) { XCTAssertNil(result, "should receive no error resetting local"); secnotice("ckksreset", "Received a rpcResetLocal(1) callback"); [resetExpectation1 fulfill]; }]; [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) { XCTAssertNil(result, "should receive no error resetting local"); secnotice("ckksreset", "Received a rpcResetLocal(2) callback"); [resetExpectation2 fulfill]; }]; // After the reset(s), we expect no uploads. Let the resets flow! [self releaseCloudKitFetchHold]; [self waitForExpectations:@[resetExpectation0, resetExpectation1, resetExpectation2] timeout:20]; XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'"); OCMVerifyAllWithDelay(self.mockDatabase, 20); [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]]; [self addGenericPassword:@"asdf" account:@"account-class-A" viewHint:nil access:(id)kSecAttrAccessibleWhenUnlocked expecting:errSecSuccess message:@"Adding class A item"]; OCMVerifyAllWithDelay(self.mockDatabase, 20); } -(void)testResetCloudKitZone { self.silentZoneDeletesAllowed = true; // Test starts with nothing in database, but one in our fake CloudKit. [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID]; [self expectCKKSTLKSelfShareUpload:self.keychainZoneID]; [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID]; // Spin up CKKS subsystem. [self startCKKSSubsystem]; // We expect a single record to be uploaded [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]]; [self addGenericPassword: @"data" account: @"account-delete-me"]; OCMVerifyAllWithDelay(self.mockDatabase, 20); [self waitForCKModifications]; // After the reset, we expect a key hierarchy upload, and then the class C item upload [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID]; [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]]; XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"]; [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) { XCTAssertNil(result, "no error resetting cloudkit"); secnotice("ckks", "Received a resetCloudKit callback"); [resetExpectation fulfill]; }]; [self waitForExpectations:@[resetExpectation] timeout:20]; OCMVerifyAllWithDelay(self.mockDatabase, 20); [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]]; [self addGenericPassword:@"asdf" account:@"account-class-A" viewHint:nil access:(id)kSecAttrAccessibleWhenUnlocked expecting:errSecSuccess message:@"Adding class A item"]; OCMVerifyAllWithDelay(self.mockDatabase, 20); } - (void)testResetCloudKitZoneDuringWaitForTLK { self.silentZoneDeletesAllowed = true; // Test starts with nothing in database, but one in our fake CloudKit. // No TLK, though! [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID]; [self putFakeDeviceStatusInCloudKit:self.keychainZoneID]; // Spin up CKKS subsystem. [self startCKKSSubsystem]; // No records should be uploaded [self addGenericPassword: @"data" account: @"account-delete-me"]; XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS should have entered waitfortlk"); // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going) self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName]; XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk"); CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil]; XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold"); // Now, reset everything. The outgoingOp should get cancelled. // We expect a key hierarchy upload, and then the class C item upload [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID]; [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]]; XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"]; [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) { XCTAssertNil(result, "no error resetting cloudkit"); [resetExpectation fulfill]; }]; [self waitForExpectations:@[resetExpectation] timeout:20]; XCTAssertTrue([outgoingOp isCancelled], "old stuck ProcessOutgoingQueue should be cancelled"); OCMVerifyAllWithDelay(self.mockDatabase, 20); // And adding another item works too [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]]; [self addGenericPassword:@"asdf" account:@"account-class-A" viewHint:nil access:(id)kSecAttrAccessibleWhenUnlocked expecting:errSecSuccess message:@"Adding class A item"]; OCMVerifyAllWithDelay(self.mockDatabase, 20); } /* * This test doesn't work, since the resetLocal fails. CKKS gets back into waitfortlk * but that isn't considered a successful resetLocal. * - (void)testResetLocalDuringWaitForTLK { // Test starts with nothing in database, but one in our fake CloudKit. // No TLK, though! [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID]; // Spin up CKKS subsystem. [self startCKKSSubsystem]; // No records should be uploaded [self addGenericPassword: @"data" account: @"account-delete-me"]; XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS should have entered waitfortlk"); // Restart CKKS to really get in the spirit of waitfortlk (and get a pending processOutgoingQueue operation going) self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName]; XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk"); CKKSOutgoingQueueOperation* outgoingOp = [self.keychainView processOutgoingQueue:nil]; XCTAssertTrue([outgoingOp isPending], "outgoing queue processing should be on hold"); // Now, reset everything. The outgoingOp should get cancelled. // We expect a key hierarchy upload, and then the class C item upload [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords:3 zoneID:self.keychainZoneID]; [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]]; XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"]; [self.injectedManager rpcResetLocal:nil reply:^(NSError* result) { XCTAssertNil(result, "no error resetting local"); [resetExpectation fulfill]; }]; [self waitForExpectations:@[resetExpectation] timeout:20]; XCTAssertTrue([outgoingOp isCancelled], "old stuck ProcessOutgoingQueue should be cancelled"); OCMVerifyAllWithDelay(self.mockDatabase, 20); // And adding another item works too [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]]; [self addGenericPassword:@"asdf" account:@"account-class-A" viewHint:nil access:(id)kSecAttrAccessibleWhenUnlocked expecting:errSecSuccess message:@"Adding class A item"]; OCMVerifyAllWithDelay(self.mockDatabase, 20); }*/ -(void)testResetCloudKitZoneWhileLoggedOut { self.silentZoneDeletesAllowed = true; // We're "logged in to" cloudkit but not in circle. self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCNotInCircle error:nil];; [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal]; self.silentFetchesAllowed = false; // Test starts with nothing in database, but one in our fake CloudKit. [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID]; // Spin up CKKS subsystem. [self startCKKSSubsystem]; CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"]; [self.keychainZone addToZone: ckr]; XCTAssertNotNil(self.keychainZone.currentDatabase, "Zone exists"); XCTAssertNotNil(self.keychainZone.currentDatabase[ckr.recordID], "An item exists in the fake zone"); XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"]; [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) { XCTAssertNil(result, "no error resetting cloudkit"); secnotice("ckks", "Received a resetCloudKit callback"); [resetExpectation fulfill]; }]; [self waitForExpectations:@[resetExpectation] timeout:20]; XCTAssertNil(self.keychainZone.currentDatabase, "No zone anymore!"); OCMVerifyAllWithDelay(self.mockDatabase, 20); // Now log in, and see what happens! It should create the zone again and upload a whole new key hierarchy self.silentFetchesAllowed = true; self.circleStatus = [[SOSAccountStatus alloc] init:kSOSCCInCircle error:nil];; [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal]; [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID]; OCMVerifyAllWithDelay(self.mockDatabase, 20); [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]]; [self addGenericPassword:@"asdf" account:@"account-class-A" viewHint:nil access:(id)kSecAttrAccessibleWhenUnlocked expecting:errSecSuccess message:@"Adding class A item"]; OCMVerifyAllWithDelay(self.mockDatabase, 20); } - (void)testResetCloudKitZoneMultipleTimes { self.silentZoneDeletesAllowed = true; // Test starts with nothing in database, but one in our fake CloudKit. [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID]; [self expectCKKSTLKSelfShareUpload:self.keychainZoneID]; [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID]; // Spin up CKKS subsystem. [self startCKKSSubsystem]; // We expect a single record to be uploaded XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'"); [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]]; [self addGenericPassword: @"data" account: @"account-delete-me"]; OCMVerifyAllWithDelay(self.mockDatabase, 20); [self waitForCKModifications]; // We're going to request a bunch of CloudKit resets, but hold them from finishing [self holdCloudKitFetches]; XCTestExpectation* resetExpectation0 = [self expectationWithDescription: @"reset callback(0) occurs"]; XCTestExpectation* resetExpectation1 = [self expectationWithDescription: @"reset callback(1) occurs"]; XCTestExpectation* resetExpectation2 = [self expectationWithDescription: @"reset callback(2) occurs"]; [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) { XCTAssertNil(result, "should receive no error resetting cloudkit"); secnotice("ckksreset", "Received a resetCloudKit(0) callback"); [resetExpectation0 fulfill]; }]; [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) { XCTAssertNil(result, "should receive no error resetting cloudkit"); secnotice("ckksreset", "Received a resetCloudKit(1) callback"); [resetExpectation1 fulfill]; }]; [self.injectedManager rpcResetCloudKit:nil reason:@"reset-test" reply:^(NSError* result) { XCTAssertNil(result, "should receive no error resetting cloudkit"); secnotice("ckksreset", "Received a resetCloudKit(2) callback"); [resetExpectation2 fulfill]; }]; // After the reset(s), we expect a key hierarchy upload, and then the class C item upload [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID]; [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]]; // And let the resets flow [self releaseCloudKitFetchHold]; [self waitForExpectations:@[resetExpectation0, resetExpectation1, resetExpectation2] timeout:20]; XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'"); OCMVerifyAllWithDelay(self.mockDatabase, 20); [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:[self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]]; [self addGenericPassword:@"asdf" account:@"account-class-A" viewHint:nil access:(id)kSecAttrAccessibleWhenUnlocked expecting:errSecSuccess message:@"Adding class A item"]; OCMVerifyAllWithDelay(self.mockDatabase, 20); } - (void)testRPCFetchAndProcessWhileCloudKitNotResponding { [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test. [self startCKKSSubsystem]; XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'"); [self holdCloudKitFetches]; XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"]; [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) { // done! we should have an underlying error of "fetch isn't working" XCTAssertNotNil(error, "Should have received an error attempting to fetch and process"); NSError* underlying = error.userInfo[NSUnderlyingErrorKey]; XCTAssertNotNil(underlying, "Should have received an underlying error"); XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain"); XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingSuccessfulFetch, "Underlying error should be 'pending fetch'"); [callbackOccurs fulfill]; }]; [self waitForExpectations:@[callbackOccurs] timeout:20]; [self releaseCloudKitFetchHold]; OCMVerifyAllWithDelay(self.mockDatabase, 20); } - (void)testRPCFetchAndProcessWhileCloudKitErroring { [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test. [self startCKKSSubsystem]; XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready'"); [self.keychainZone failNextFetchWith:[[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorRequestRateLimited userInfo:@{CKErrorRetryAfterKey : [NSNumber numberWithInt:30]}]]; XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"]; [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) { // done! we should have an underlying error of "fetch isn't working" XCTAssertNotNil(error, "Should have received an error attempting to fetch and process"); NSError* underlying = error.userInfo[NSUnderlyingErrorKey]; XCTAssertNotNil(underlying, "Should have received an underlying error"); XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain"); XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingSuccessfulFetch, "Underlying error should be 'pending fetch'"); NSError* underunderlying = underlying.userInfo[NSUnderlyingErrorKey]; XCTAssertNotNil(underunderlying, "Should have received another layer of underlying error"); XCTAssertEqualObjects(underunderlying.domain, CKErrorDomain, "Underlying error should be CKErrorDomain"); XCTAssertEqual(underunderlying.code, CKErrorRequestRateLimited, "Underlying error should be 'rate limited'"); [callbackOccurs fulfill]; }]; [self waitForExpectations:@[callbackOccurs] timeout:20]; OCMVerifyAllWithDelay(self.mockDatabase, 20); } - (void)testRPCFetchAndProcessWhileInWaitForTLK { [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID]; [self putFakeDeviceStatusInCloudKit:self.keychainZoneID]; [self startCKKSSubsystem]; XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk"); XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"]; [self.ckksControl rpcFetchAndProcessChanges:nil reply:^(NSError * _Nullable error) { // done! we should have an underlying error of "fetch isn't working" XCTAssertNotNil(error, "Should have received an error attempting to fetch and process"); NSError* underlying = error.userInfo[NSUnderlyingErrorKey]; XCTAssertNotNil(underlying, "Should have received an underlying error"); XCTAssertEqualObjects(underlying.domain, CKKSResultDescriptionErrorDomain, "Underlying error should be CKKSResultDescriptionErrorDomain"); XCTAssertEqual(underlying.code, CKKSResultDescriptionPendingKeyReady, "Underlying error should be 'pending key ready'"); [callbackOccurs fulfill]; }]; [self waitForExpectations:@[callbackOccurs] timeout:20]; OCMVerifyAllWithDelay(self.mockDatabase, 20); } - (void)testRPCTLKMissingWhenMissing { // Bring CKKS up in waitfortlk [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID]; [self putFakeDeviceStatusInCloudKit:self.keychainZoneID]; [self startCKKSSubsystem]; XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk"); XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"]; [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) { XCTAssertTrue(missing, "TLKs should be missing"); [callbackOccurs fulfill]; }]; [self waitForExpectations:@[callbackOccurs] timeout:20]; OCMVerifyAllWithDelay(self.mockDatabase, 20); } - (void)testRPCTLKMissingWhenFound { // Bring CKKS up in 'ready' [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID]; [self saveTLKMaterialToKeychain:self.keychainZoneID]; [self expectCKKSTLKSelfShareUpload:self.keychainZoneID]; [self startCKKSSubsystem]; XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready''"); XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"]; [self.ckksControl rpcTLKMissing:@"keychain" reply:^(bool missing) { XCTAssertFalse(missing, "TLKs should not be missing"); [callbackOccurs fulfill]; }]; [self waitForExpectations:@[callbackOccurs] timeout:20]; OCMVerifyAllWithDelay(self.mockDatabase, 20); } - (void)testRPCKnownBadStateWhenTLKsMissing { // Bring CKKS up in waitfortlk [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID]; [self putFakeDeviceStatusInCloudKit:self.keychainZoneID]; [self startCKKSSubsystem]; XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk"); XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"]; [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) { XCTAssertEqual(result, CKKSKnownStateTLKsMissing, "TLKs should be missing"); [callbackOccurs fulfill]; }]; [self waitForExpectations:@[callbackOccurs] timeout:20]; OCMVerifyAllWithDelay(self.mockDatabase, 20); } - (void)testRPCKnownBadStateWhenInWaitForUnlock { // Bring CKKS up in 'waitfortunlok' self.aksLockState = true; [self.lockStateTracker recheck]; [self startCKKSSubsystem]; // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur. XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:20*NSEC_PER_SEC], @"Key state should get stuck in waitforunlock"); XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"]; [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) { XCTAssertEqual(result, CKKSKnownStateWaitForUnlock, "known state should be wait for unlock"); [callbackOccurs fulfill]; }]; [self waitForExpectations:@[callbackOccurs] timeout:20]; OCMVerifyAllWithDelay(self.mockDatabase, 20); } - (void)testRPCKnownBadStateWhenInGoodState { // Bring CKKS up in 'ready' [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID]; [self saveTLKMaterialToKeychain:self.keychainZoneID]; [self expectCKKSTLKSelfShareUpload:self.keychainZoneID]; [self startCKKSSubsystem]; XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered 'ready''"); XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"]; [self.ckksControl rpcKnownBadState:@"keychain" reply:^(CKKSKnownBadState result) { XCTAssertEqual(result, CKKSKnownStatePossiblyGood, "known state should not be possibly-good"); [callbackOccurs fulfill]; }]; [self waitForExpectations:@[callbackOccurs] timeout:20]; OCMVerifyAllWithDelay(self.mockDatabase, 20); } - (void)testRpcStatus { [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test. [self startCKKSSubsystem]; // Let things shake themselves out. OCMVerifyAllWithDelay(self.mockDatabase, 20); XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should return to 'ready'"); [self waitForCKModifications]; XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"]; [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) { XCTAssertNil(error, "should be no error fetching status for keychain"); // Ugly "global" hack XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back"); NSDictionary* keychainStatus = result[1]; XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back"); XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view"); XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status"); [callbackOccurs fulfill]; }]; [self waitForExpectations:@[callbackOccurs] timeout:20]; } - (void)testRpcStatusWaitsForAccountDetermination { [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test. // Set up the account state callbacks to happen in one second dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (1 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ // Let CKKS come up (simulating daemon starting due to RPC) [self startCKKSSubsystem]; }); // Before CKKS figures out we're in an account, fire off the status RPC. XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"]; [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) { XCTAssertNil(error, "should be no error fetching status for keychain"); // Ugly "global" hack XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back"); NSDictionary* keychainStatus = result[1]; XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back"); XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view"); XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateReady, "Should be in 'ready' status"); [callbackOccurs fulfill]; }]; [self waitForExpectations:@[callbackOccurs] timeout:20]; } - (void)testRpcStatusIsFastDuringError { [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test. self.keychainFetchError = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecInternalError description:@"injected keychain failure"]; // Let CKKS come up; it should enter 'error' [self startCKKSSubsystem]; XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateError] wait:20*NSEC_PER_SEC], "CKKS entered 'error'"); // Fire off the status RPC; it should return immediately XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"]; [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) { XCTAssertNil(error, "should be no error fetching status for keychain"); // Ugly "global" hack XCTAssertEqual(result.count, 2u, "Should have received two result dictionaries back"); NSDictionary* keychainStatus = result[1]; XCTAssertNotNil(keychainStatus, "Should have received at least one zone status back"); XCTAssertEqualObjects(keychainStatus[@"view"], @"keychain", "Should have received status for the keychain view"); XCTAssertEqualObjects(keychainStatus[@"keystate"], SecCKKSZoneKeyStateError, "Should be in 'ready' status"); [callbackOccurs fulfill]; }]; [self waitForExpectations:@[callbackOccurs] timeout:20]; } @end #endif // OCTAGON