CKKSTests+ForwardCompatibility.m [plain text]
/*
* Copyright (c) 2020 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>
#import <TrustedPeers/TrustedPeers.h>
#import <TrustedPeers/TPPBPolicyKeyViewMapping.h>
#import <TrustedPeers/TPDictionaryMatchingRules.h>
#import "keychain/categories/NSError+UsefulConstructors.h"
#import "keychain/ckks/CKKS.h"
#import "keychain/ckks/CKKSIncomingQueueEntry.h"
#import "keychain/ckks/CKKSOutgoingQueueEntry.h"
#import "keychain/ckks/CloudKitCategories.h"
#import "keychain/ckks/tests/CKKSTests.h"
#import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
#import "keychain/ckks/tests/CloudKitMockXCTest.h"
#import "keychain/ckks/tests/MockCloudKit.h"
@interface CloudKitKeychainForwardCompatibilityTests : CloudKitKeychainSyncingTestsBase
@property CKRecordZoneID* unknownZoneID;
@property CKRecordZoneID* passwordsZoneID;
@property CKKSKeychainView* passwordsView;
@property TPSyncingPolicy* originalPolicy;
@property TPSyncingPolicy* originalPolicyPlusUnknownVwht;
@property TPSyncingPolicy* allItemsToPasswordsPolicy;
@end
@implementation CloudKitKeychainForwardCompatibilityTests
- (void)setUp {
[super setUp];
self.passwordsZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Passwords" ownerName:CKCurrentUserDefaultName];
self.unknownZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"unknown-zone" ownerName:CKCurrentUserDefaultName];
self.zones[self.passwordsZoneID] = [[FakeCKZone alloc] initZone:self.passwordsZoneID];
self.originalPolicy = self.viewSortingPolicyForManagedViewList;
NSMutableArray<TPPBPolicyKeyViewMapping*>* newRules = [self.originalPolicy.keyViewMapping mutableCopy];
TPPBPolicyKeyViewMapping* unknownVwhtMapping = [[TPPBPolicyKeyViewMapping alloc] init];
unknownVwhtMapping.view = self.keychainZoneID.zoneName;
unknownVwhtMapping.matchingRule = [TPDictionaryMatchingRule fieldMatch:@"vwht"
fieldRegex:[NSString stringWithFormat:@"^ [newRules insertObject:unknownVwhtMapping atIndex:0];
self.originalPolicyPlusUnknownVwht = [[TPSyncingPolicy alloc] initWithModel:@"test-policy"
version:[[TPPolicyVersion alloc] initWithVersion:2 hash:@"fake-policy-for-views-with-unknown-view"]
viewList:self.originalPolicy.viewList
priorityViews:self.originalPolicy.priorityViews
userControllableViews:self.originalPolicy.userControllableViews
syncUserControllableViews:self.originalPolicy.syncUserControllableViews
viewsToPiggybackTLKs:self.originalPolicy.viewsToPiggybackTLKs
keyViewMapping:newRules];
TPPBPolicyKeyViewMapping* passwordsVwhtMapping = [[TPPBPolicyKeyViewMapping alloc] init];
passwordsVwhtMapping.view = self.passwordsZoneID.zoneName;
passwordsVwhtMapping.matchingRule = [TPDictionaryMatchingRule trueMatch];
self.allItemsToPasswordsPolicy = [[TPSyncingPolicy alloc] initWithModel:@"test-policy"
version:[[TPPolicyVersion alloc] initWithVersion:2 hash:@"fake-policy-for-views-with-passwords-view"]
viewList:[NSSet setWithArray:@[self.keychainView.zoneName, self.passwordsZoneID.zoneName]]
priorityViews:[NSSet set]
userControllableViews:self.originalPolicy.userControllableViews
syncUserControllableViews:self.originalPolicy.syncUserControllableViews
viewsToPiggybackTLKs:self.originalPolicy.viewsToPiggybackTLKs
keyViewMapping:@[passwordsVwhtMapping]];
}
- (void)setPolicyAndWaitForQuiescence:(TPSyncingPolicy*)policy policyIsFresh:(BOOL)policyIsFresh {
[self.injectedManager setCurrentSyncingPolicy:policy policyIsFresh:policyIsFresh];
self.ckksViews = [NSMutableSet setWithArray:[[self.injectedManager views] allValues]];
[self beginSOSTrustedOperationForAllViews];
// And wait for everything to enter a resting state
for(CKKSKeychainView* view in self.ckksViews) {
XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
[view waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
[view waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
}
}
- (void)testReceiveItemForWrongView {
self.requestPolicyCheck = OCMClassMock([CKKSNearFutureScheduler class]);
OCMExpect([self.requestPolicyCheck trigger]);
[self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
[self startCKKSSubsystem];
XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
NSString* wrongZoneAccount = @"wrong-zone";
NSDictionary* item = [self fakeRecordDictionary:wrongZoneAccount zoneID:self.unknownZoneID];
CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" zoneID:self.keychainZoneID];
CKRecord* ckr = [self newRecord:ckrid withNewItemData:item];
[self.keychainZone addToZone:ckr];
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
[self.keychainView waitForFetchAndIncomingQueueProcessing];
[self findGenericPassword:wrongZoneAccount expecting:errSecItemNotFound];
OCMVerifyAllWithDelay(self.requestPolicyCheck, 10);
NSError* zoneError = nil;
NSInteger count = [CKKSIncomingQueueEntry countByState:SecCKKSStateMismatchedView zone:self.keychainView.zoneID error:&zoneError];
XCTAssertNil(zoneError, "should be no error counting all IQEs");
XCTAssertEqual(count, 1, "Should be one mismatched IQE");
}
- (void)testConflictingItemInWrongView {
self.requestPolicyCheck = OCMClassMock([CKKSNearFutureScheduler class]);
OCMExpect([self.requestPolicyCheck trigger]);
[self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
[self createAndSaveFakeKeyHierarchy:self.passwordsZoneID];
[self startCKKSSubsystem];
[self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:NO];
self.passwordsView = [self.injectedManager findView:@"Passwords"];
XCTAssertNotNil(self.passwordsView, @"Policy created a passwords view");
XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
XCTAssertEqual(0, [self.passwordsView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
NSDictionary* item = [self fakeRecordDictionary:@"account-delete-me" zoneID:self.passwordsZoneID];
CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" zoneID:self.passwordsZoneID];
CKRecordID* ckr2id = [[CKRecordID alloc] initWithRecordName:@"FFFF8D31-F9C5-481E-98AC-5A507ACB2D85" zoneID:self.keychainZoneID];
CKRecord* ckr = [self newRecord:ckrid withNewItemData:item];
NSMutableDictionary* item2 = [item mutableCopy];
item2[@"v_Data"] = @"wrongview";
CKRecord* ckr2 = [self newRecord:ckr2id withNewItemData:item2];
// Receive the passwords item first
[self.zones[self.passwordsZoneID] addToZone:ckr];
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
[self.passwordsView waitForFetchAndIncomingQueueProcessing];
[self checkGenericPassword:@"data" account:@"account-delete-me"];
[self.zones[self.keychainZoneID] addToZone:ckr2];
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
[self.keychainView waitForFetchAndIncomingQueueProcessing];
// THe view should ask for an update, and receive one
OCMVerifyAllWithDelay(self.requestPolicyCheck, 10);
[self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:YES];
// And we have ignored the change in the other view
[self checkGenericPassword:@"data" account:@"account-delete-me"];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
// And the item is then deleted
[self.zones[self.keychainZoneID] deleteCKRecordIDFromZone:ckr2id];
OCMExpect([self.requestPolicyCheck trigger]);
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
[self.keychainView waitForFetchAndIncomingQueueProcessing];
[self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:YES];
// The password should still exist
[self checkGenericPassword:@"data" account:@"account-delete-me"];
}
- (void)testConflictingItemInWrongViewWithLowerUUID {
self.requestPolicyCheck = OCMClassMock([CKKSNearFutureScheduler class]);
OCMExpect([self.requestPolicyCheck trigger]);
[self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
[self createAndSaveFakeKeyHierarchy:self.passwordsZoneID];
[self startCKKSSubsystem];
[self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:NO];
self.passwordsView = [self.injectedManager findView:@"Passwords"];
XCTAssertNotNil(self.passwordsView, @"Policy created a passwords view");
XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
XCTAssertEqual(0, [self.passwordsView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
NSDictionary* item = [self fakeRecordDictionary:@"account-delete-me" zoneID:self.passwordsZoneID];
CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" zoneID:self.passwordsZoneID];
CKRecordID* ckr2id = [[CKRecordID alloc] initWithRecordName:@"00008D31-F9C5-481E-98AC-5A507ACB2D85" zoneID:self.keychainZoneID];
CKRecord* ckr = [self newRecord:ckrid withNewItemData:item];
NSMutableDictionary* item2 = [item mutableCopy];
item2[@"v_Data"] = @"wrongview";
CKRecord* ckr2 = [self newRecord:ckr2id withNewItemData:item2];
// Receive the passwords item first
[self.zones[self.passwordsZoneID] addToZone:ckr];
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
[self.passwordsView waitForFetchAndIncomingQueueProcessing];
[self checkGenericPassword:@"data" account:@"account-delete-me"];
[self.zones[self.keychainZoneID] addToZone:ckr2];
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
[self.keychainView waitForFetchAndIncomingQueueProcessing];
// THe view should ask for an update, and receive one
OCMVerifyAllWithDelay(self.requestPolicyCheck, 10);
[self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:YES];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self checkGenericPassword:@"data" account:@"account-delete-me"];
// And the item is then deleted
[self.zones[self.keychainZoneID] deleteCKRecordIDFromZone:ckr2id];
OCMExpect([self.requestPolicyCheck trigger]);
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
[self.keychainView waitForFetchAndIncomingQueueProcessing];
[self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:YES];
// The password should still exist
[self checkGenericPassword:@"data" account:@"account-delete-me"];
}
- (void)testConflictingItemInWrongViewWithSameUUID {
self.requestPolicyCheck = OCMClassMock([CKKSNearFutureScheduler class]);
OCMExpect([self.requestPolicyCheck trigger]);
[self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
[self createAndSaveFakeKeyHierarchy:self.passwordsZoneID];
[self startCKKSSubsystem];
[self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:NO];
self.passwordsView = [self.injectedManager findView:@"Passwords"];
XCTAssertNotNil(self.passwordsView, @"Policy created a passwords view");
XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
XCTAssertEqual(0, [self.passwordsView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
NSDictionary* item = [self fakeRecordDictionary:@"account-delete-me" zoneID:self.passwordsZoneID];
CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" zoneID:self.passwordsZoneID];
CKRecordID* ckr2id = [[CKRecordID alloc] initWithRecordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" zoneID:self.keychainZoneID];
CKRecord* ckr = [self newRecord:ckrid withNewItemData:item];
NSMutableDictionary* item2 = [item mutableCopy];
item2[@"v_Data"] = @"wrongview";
CKRecord* ckr2 = [self newRecord:ckr2id withNewItemData:item2];
// Receive the passwords item first
[self.zones[self.passwordsZoneID] addToZone:ckr];
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
[self.passwordsView waitForFetchAndIncomingQueueProcessing];
[self checkGenericPassword:@"data" account:@"account-delete-me"];
[self.zones[self.keychainZoneID] addToZone:ckr2];
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
[self.keychainView waitForFetchAndIncomingQueueProcessing];
// THe view should ask for a policy update, and receive one
OCMVerifyAllWithDelay(self.requestPolicyCheck, 10);
[self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:YES];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self checkGenericPassword:@"data" account:@"account-delete-me"];
// And the item is then deleted
[self.zones[self.keychainZoneID] deleteCKRecordIDFromZone:ckr2id];
OCMExpect([self.requestPolicyCheck trigger]);
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
[self.keychainView waitForFetchAndIncomingQueueProcessing];
[self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:YES];
// The password should still exist
[self checkGenericPassword:@"data" account:@"account-delete-me"];
}
- (void)testReceiveItemForFuturePolicy {
self.requestPolicyCheck = OCMClassMock([CKKSNearFutureScheduler class]);
OCMExpect([self.requestPolicyCheck trigger]);
[self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
[self startCKKSSubsystem];
XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
NSString* wrongZoneAccount = @"wrong-zone";
NSDictionary* item = [self fakeRecordDictionary:wrongZoneAccount zoneID:self.unknownZoneID];
CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" zoneID:self.keychainZoneID];
CKRecord* ckr = [self newRecord:ckrid withNewItemData:item];
[self.keychainZone addToZone:ckr];
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
[self.keychainView waitForFetchAndIncomingQueueProcessing];
[self findGenericPassword:wrongZoneAccount expecting:errSecItemNotFound];
OCMVerifyAllWithDelay(self.requestPolicyCheck, 10);
// Now, Octagon discovers that there's a new policy that allows this item in the keychain view
TPSyncingPolicy* currentPolicy = self.injectedManager.policy;
XCTAssertNotNil(currentPolicy, "should have a current policy");
[self setPolicyAndWaitForQuiescence:self.originalPolicyPlusUnknownVwht policyIsFresh:YES];
[self findGenericPassword:wrongZoneAccount expecting:errSecSuccess];
}
- (void)testHandleItemMovedBetweenViewsBeforePolicyChange {
[self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
[self createAndSaveFakeKeyHierarchy:self.passwordsZoneID];
[self startCKKSSubsystem];
XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
// A password is created and uploaded out of the keychain view.
__block CKRecord* itemCKRecord = nil;
[self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:^BOOL(CKRecord * _Nonnull record) {
itemCKRecord = record;
return YES;
}];
NSString* itemAccount = @"account-delete-me";
[self addGenericPassword:@"data" account:itemAccount viewHint:self.keychainZoneID.zoneName];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
XCTAssertNotNil(itemCKRecord, "Should have some CKRecord for the added item");
// Update etag as well
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
[self.keychainView waitForFetchAndIncomingQueueProcessing];
// Another device shows up, changes the item sync policy, and moves the item over into the passwords view.
NSDictionary* itemContents = [self decryptRecord:itemCKRecord];
XCTAssertNotNil(itemContents, "should have some item contents");
CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:itemCKRecord.recordID.recordName zoneID:self.passwordsZoneID];
CKRecord* ckr = [self newRecord:ckrid withNewItemData:itemContents];
[self.zones[self.passwordsZoneID] addToZone:ckr];
TPSyncingPolicy* currentPolicy = self.injectedManager.policy;
XCTAssertNotNil(currentPolicy, "should have a current policy");
// In this test, we receive the deletion, and then the policy change adding the Passwords view
[self.keychainZone deleteCKRecordIDFromZone:itemCKRecord.recordID];
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
[self.keychainView waitForFetchAndIncomingQueueProcessing];
[self findGenericPassword:itemAccount expecting:errSecItemNotFound];
[self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:NO];
// And once passwords syncs, the item should appear again
[self findGenericPassword:itemAccount expecting:errSecSuccess];
}
- (void)testHandleItemMovedBetweenViewsAfterPolicyChange {
self.requestPolicyCheck = OCMClassMock([CKKSNearFutureScheduler class]);
OCMExpect([self.requestPolicyCheck trigger]);
[self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
[self createAndSaveFakeKeyHierarchy:self.passwordsZoneID];
[self startCKKSSubsystem];
XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
// A password is created and uploaded out of the keychain view.
__block CKRecord* itemCKRecord = nil;
[self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:^BOOL(CKRecord * _Nonnull record) {
itemCKRecord = record;
return YES;
}];
NSString* itemAccount = @"account-delete-me";
[self addGenericPassword:@"data" account:itemAccount viewHint:self.keychainZoneID.zoneName];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
XCTAssertNotNil(itemCKRecord, "Should have some CKRecord for the added item");
// Update etag as well
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
[self.keychainView waitForFetchAndIncomingQueueProcessing];
// Another device shows up, changes the item sync policy, and moves the item over into the Passwords view.
// But, in this case, we receive the item delete in the Keychain view after we've synced the item in the Passwords view
NSDictionary* itemContents = [self decryptRecord:itemCKRecord];
XCTAssertNotNil(itemContents, "should have some item contents");
CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:itemCKRecord.recordID.recordName zoneID:self.passwordsZoneID];
CKRecord* ckr = [self newRecord:ckrid withNewItemData:itemContents];
[self.zones[self.passwordsZoneID] addToZone:ckr];
TPSyncingPolicy* currentPolicy = self.injectedManager.policy;
XCTAssertNotNil(currentPolicy, "should have a current policy");
[self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:NO];
CKKSKeychainView* passwordsView = [self.injectedManager findView:self.passwordsZoneID.zoneName];
XCTAssertNotNil(passwordsView, @"Should have a passwords view");
// And once Passwords syncs, the item should appear again
[self findGenericPassword:itemAccount expecting:errSecSuccess];
// And now we receive the delete in the keychain view. The item should still exist!
[self.keychainZone deleteCKRecordIDFromZone:itemCKRecord.recordID];
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
[self.keychainView waitForFetchAndIncomingQueueProcessing];
[self findGenericPassword:itemAccount expecting:errSecSuccess];
// Keychain View should have asked for a policy set
OCMVerifyAllWithDelay(self.requestPolicyCheck, 10);
[self.injectedManager setCurrentSyncingPolicy:self.allItemsToPasswordsPolicy policyIsFresh:YES];
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
[self.keychainView waitForFetchAndIncomingQueueProcessing];
[self findGenericPassword:itemAccount expecting:errSecSuccess];
NSError* zoneError = nil;
NSInteger count = [CKKSIncomingQueueEntry countByState:SecCKKSStateMismatchedView zone:self.keychainView.zoneID error:&zoneError];
XCTAssertNil(zoneError, "should be no error counting all IQEs");
XCTAssertEqual(count, 0, "Should be no remaining mismatched IQEs");
}
- (void)testMoveItemUploadedToOldZone {
self.requestPolicyCheck = OCMClassMock([CKKSNearFutureScheduler class]);
OCMExpect([self.requestPolicyCheck trigger]);
[self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
[self createAndSaveFakeKeyHierarchy:self.passwordsZoneID];
[self startCKKSSubsystem];
XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");
[self setPolicyAndWaitForQuiescence:self.allItemsToPasswordsPolicy policyIsFresh:NO];
// Now, someone uploads an item to the keychain view. We should move it to the passwords view
CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"account0"];
[self.keychainZone addToZone:ckr];
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
[self.keychainView waitForFetchAndIncomingQueueProcessing];
// The keychain view should request a policy refetch
OCMVerifyAllWithDelay(self.requestPolicyCheck, 10);
// Note that ideally, we'd remove the old item from CloudKit. But, other devices which participate in CKKS4All
// might not have this forward-compatiblity change, and will treat this as a deletion. If they process this deletion,
// then sync the resulting tombstone to a third SOS device, then receive the addition in the 'right' view, and then the
// tombstone syncs back to the CKKS4All devices, then we might end up deleting the item across the account.
// Until enough internal folk have moved onto builds with this forward-compat change, we can't issue these deletes.
//[self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
// The item should be reuploaded to Passwords, though.
[self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.passwordsZoneID checkItem:^BOOL(CKRecord * _Nonnull record) {
return [record.recordID.recordName isEqualToString:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
}];
[self.injectedManager setCurrentSyncingPolicy:self.allItemsToPasswordsPolicy policyIsFresh:YES];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
// And the keychain item should still exist
[self findGenericPassword:@"account0" expecting:errSecSuccess];
}
@end
#endif // OCTAGON