CKKSTests+MultiZone.m [plain text]
#if OCTAGON
#import <CloudKit/CloudKit.h>
#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>
#import <notify.h>
#include <Security/SecItemPriv.h>
#import <TrustedPeers/TrustedPeers.h>
#import <TrustedPeers/TPPBPolicyKeyViewMapping.h>
#import <TrustedPeers/TPDictionaryMatchingRules.h>
#import "keychain/ckks/tests/CloudKitMockXCTest.h"
#import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
#import "keychain/ckks/CKKS.h"
#import "keychain/ckks/CKKSKeychainView.h"
#import "keychain/ckks/CKKSCurrentKeyPointer.h"
#import "keychain/ckks/CKKSItemEncrypter.h"
#import "keychain/ckks/CKKSKey.h"
#import "keychain/ckks/CKKSOutgoingQueueEntry.h"
#import "keychain/ckks/CKKSIncomingQueueEntry.h"
#import "keychain/ckks/CKKSStates.h"
#import "keychain/ckks/CKKSSynchronizeOperation.h"
#import "keychain/ckks/CKKSViewManager.h"
#import "keychain/ckks/CKKSZoneStateEntry.h"
#import "keychain/ckks/CKKSManifest.h"
#import "keychain/ckks/tests/CKKSTests+MultiZone.h"
#import "keychain/ckks/tests/MockCloudKit.h"
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#include <Security/SecureObjectSync/SOSCloudCircle.h>
#include "keychain/SecureObjectSync/SOSAccountPriv.h"
#include "keychain/SecureObjectSync/SOSAccount.h"
#include "keychain/SecureObjectSync/SOSInternal.h"
#include "keychain/SecureObjectSync/SOSFullPeerInfo.h"
#pragma clang diagnostic pop
#include <Security/SecKey.h>
#include <Security/SecKeyPriv.h>
#pragma clang diagnostic pop
@interface CloudKitKeychainSyncingMultiZoneTestsBase ()
@end
@implementation CloudKitKeychainSyncingMultiZoneTestsBase
+ (void)setUp {
SecCKKSEnable();
SecCKKSResetSyncing();
[super setUp];
}
- (NSSet*)managedViewList {
NSMutableSet* parentSet = [[super managedViewList] mutableCopy];
[parentSet addObject:@"Passwords"];
return parentSet;
}
// Make a policy as normal for most views, but Passwords is special
- (TPSyncingPolicy*)viewSortingPolicyForManagedViewList
{
NSMutableArray<TPPBPolicyKeyViewMapping*>* rules = [NSMutableArray array];
for(NSString* viewName in self.managedViewList) {
TPPBPolicyKeyViewMapping* mapping = [[TPPBPolicyKeyViewMapping alloc] init];
mapping.view = viewName;
// The real passwords view is on com.appple.cfnetwork, but for these tests, let's just use the sbd agrp (because of how the entitlements are specified)
if([viewName isEqualToString:@"Passwords"]) {
mapping.matchingRule = [TPDictionaryMatchingRule fieldMatch:@"agrp"
fieldRegex:[NSString stringWithFormat:@"^com\\.apple\\.sbd$"]];
} else {
mapping.matchingRule = [TPDictionaryMatchingRule fieldMatch:@"vwht"
fieldRegex:[NSString stringWithFormat:@"^ }
[rules addObject:mapping];
}
NSSet<NSString*>* viewList = [self managedViewList];
TPSyncingPolicy* policy = [[TPSyncingPolicy alloc] initWithModel:@"test-policy"
version:[[TPPolicyVersion alloc] initWithVersion:1 hash:@"fake-policy-for-views"]
viewList:viewList
priorityViews:[NSSet set]
userControllableViews:[NSSet set]
syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_ENABLED
viewsToPiggybackTLKs:[viewList containsObject:@"Passwords"] ? [NSSet setWithObject:@"Passwords"] : [NSSet set]
keyViewMapping:rules];
return policy;
}
- (void)setUp {
SecCKKSSetSyncManifests(false);
SecCKKSSetEnforceManifests(false);
[super setUp];
SecCKKSTestSetDisableSOS(false);
// Wait for the ViewManager to be brought up
XCTAssertEqual(0, [self.injectedManager.completedSecCKKSInitialize wait:20*NSEC_PER_SEC], "No timeout waiting for SecCKKSInitialize");
self.engramZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Engram" ownerName:CKCurrentUserDefaultName];
self.engramZone = [[FakeCKZone alloc] initZone: self.engramZoneID];
self.zones[self.engramZoneID] = self.engramZone;
self.engramView = [[CKKSViewManager manager] findOrCreateView:@"Engram"];
XCTAssertNotNil(self.engramView, "CKKSViewManager created the Engram view");
[self.ckksViews addObject:self.engramView];
[self.ckksZones addObject:self.engramZoneID];
self.manateeZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Manatee" ownerName:CKCurrentUserDefaultName];
self.manateeZone = [[FakeCKZone alloc] initZone: self.manateeZoneID];
self.zones[self.manateeZoneID] = self.manateeZone;
self.manateeView = [[CKKSViewManager manager] findOrCreateView:@"Manatee"];
XCTAssertNotNil(self.manateeView, "CKKSViewManager created the Manatee view");
[self.ckksViews addObject:self.manateeView];
[self.ckksZones addObject:self.manateeZoneID];
self.autoUnlockZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"AutoUnlock" ownerName:CKCurrentUserDefaultName];
self.autoUnlockZone = [[FakeCKZone alloc] initZone: self.autoUnlockZoneID];
self.zones[self.autoUnlockZoneID] = self.autoUnlockZone;
self.autoUnlockView = [[CKKSViewManager manager] findOrCreateView:@"AutoUnlock"];
XCTAssertNotNil(self.autoUnlockView, "CKKSViewManager created the AutoUnlock view");
[self.ckksViews addObject:self.autoUnlockView];
[self.ckksZones addObject:self.autoUnlockZoneID];
self.healthZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Health" ownerName:CKCurrentUserDefaultName];
self.healthZone = [[FakeCKZone alloc] initZone: self.healthZoneID];
self.zones[self.healthZoneID] = self.healthZone;
self.healthView = [[CKKSViewManager manager] findOrCreateView:@"Health"];
XCTAssertNotNil(self.healthView, "CKKSViewManager created the Health view");
[self.ckksViews addObject:self.healthView];
[self.ckksZones addObject:self.healthZoneID];
self.applepayZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"ApplePay" ownerName:CKCurrentUserDefaultName];
self.applepayZone = [[FakeCKZone alloc] initZone: self.applepayZoneID];
self.zones[self.applepayZoneID] = self.applepayZone;
self.applepayView = [[CKKSViewManager manager] findOrCreateView:@"ApplePay"];
XCTAssertNotNil(self.applepayView, "CKKSViewManager created the ApplePay view");
[self.ckksViews addObject:self.applepayView];
[self.ckksZones addObject:self.applepayZoneID];
self.homeZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Home" ownerName:CKCurrentUserDefaultName];
self.homeZone = [[FakeCKZone alloc] initZone: self.homeZoneID];
self.zones[self.homeZoneID] = self.homeZone;
self.homeView = [[CKKSViewManager manager] findOrCreateView:@"Home"];
XCTAssertNotNil(self.homeView, "CKKSViewManager created the Home view");
[self.ckksViews addObject:self.homeView];
[self.ckksZones addObject:self.homeZoneID];
self.limitedZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"LimitedPeersAllowed" ownerName:CKCurrentUserDefaultName];
self.limitedZone = [[FakeCKZone alloc] initZone: self.limitedZoneID];
self.zones[self.limitedZoneID] = self.limitedZone;
self.limitedView = [[CKKSViewManager manager] findOrCreateView:@"LimitedPeersAllowed"];
XCTAssertNotNil(self.limitedView, "should have a limited ckks view");
XCTAssertNotNil(self.limitedView, "CKKSViewManager created the LimitedPeersAllowed view");
[self.ckksViews addObject:self.limitedView];
[self.ckksZones addObject:self.limitedZoneID];
self.passwordsZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Passwords" ownerName:CKCurrentUserDefaultName];
self.passwordsZone = [[FakeCKZone alloc] initZone: self.passwordsZoneID];
self.zones[self.passwordsZoneID] = self.passwordsZone;
self.passwordsView = [[CKKSViewManager manager] findOrCreateView:@"Passwords"];
XCTAssertNotNil(self.passwordsView, "should have a passwords ckks view");
XCTAssertNotNil(self.passwordsView, "CKKSViewManager created the Passwords view");
[self.ckksViews addObject:self.passwordsView];
[self.ckksZones addObject:self.passwordsZoneID];
// These tests, at least, will use the policy codepaths!
[self.injectedManager setOverrideCKKSViewsFromPolicy:YES];
[self.injectedManager setCurrentSyncingPolicy:self.viewSortingPolicyForManagedViewList];
}
+ (void)tearDown {
SecCKKSTestSetDisableSOS(true);
[super tearDown];
SecCKKSResetSyncing();
}
- (void)tearDown {
// If the test didn't already do this, allow each zone to spin up
self.accountStatus = CKAccountStatusNoAccount;
[self startCKKSSubsystem];
[self.engramView halt];
[self.engramView waitUntilAllOperationsAreFinished];
self.engramView = nil;
[self.manateeView halt];
[self.manateeView waitUntilAllOperationsAreFinished];
self.manateeView = nil;
[self.autoUnlockView halt];
[self.autoUnlockView waitUntilAllOperationsAreFinished];
self.autoUnlockView = nil;
[self.healthView halt];
[self.healthView waitUntilAllOperationsAreFinished];
self.healthView = nil;
[self.applepayView halt];
[self.applepayView waitUntilAllOperationsAreFinished];
self.applepayView = nil;
[self.homeView halt];
[self.homeView waitUntilAllOperationsAreFinished];
self.homeView = nil;
[self.limitedView halt];
[self.limitedView waitUntilAllOperationsAreFinished];
self.limitedView = nil;
[self.passwordsView halt];
[self.passwordsView waitUntilAllOperationsAreFinished];
self.passwordsView = nil;
[super tearDown];
}
- (ZoneKeys*)engramZoneKeys {
return self.keys[self.engramZoneID];
}
- (ZoneKeys*)manateeZoneKeys {
return self.keys[self.manateeZoneID];
}
- (void)saveFakeKeyHierarchiesToLocalDatabase {
for(CKRecordZoneID* zoneID in self.ckksZones) {
[self createAndSaveFakeKeyHierarchy: zoneID];
}
}
- (void)putFakeDeviceStatusesInCloudKit {
[self putFakeDeviceStatusInCloudKit: self.engramZoneID];
[self putFakeDeviceStatusInCloudKit: self.manateeZoneID];
[self putFakeDeviceStatusInCloudKit: self.autoUnlockZoneID];
[self putFakeDeviceStatusInCloudKit: self.healthZoneID];
[self putFakeDeviceStatusInCloudKit: self.applepayZoneID];
[self putFakeDeviceStatusInCloudKit: self.homeZoneID];
[self putFakeDeviceStatusInCloudKit: self.limitedZoneID];
[self putFakeDeviceStatusInCloudKit: self.passwordsZoneID];
}
- (void)putFakeKeyHierachiesInCloudKit{
[self putFakeKeyHierarchyInCloudKit: self.engramZoneID];
[self putFakeKeyHierarchyInCloudKit: self.manateeZoneID];
[self putFakeKeyHierarchyInCloudKit: self.autoUnlockZoneID];
[self putFakeKeyHierarchyInCloudKit: self.healthZoneID];
[self putFakeKeyHierarchyInCloudKit: self.applepayZoneID];
[self putFakeKeyHierarchyInCloudKit: self.homeZoneID];
[self putFakeKeyHierarchyInCloudKit: self.limitedZoneID];
[self putFakeKeyHierarchyInCloudKit: self.passwordsZoneID];
}
- (void)saveTLKsToKeychain{
[self saveTLKMaterialToKeychain:self.engramZoneID];
[self saveTLKMaterialToKeychain:self.manateeZoneID];
[self saveTLKMaterialToKeychain:self.autoUnlockZoneID];
[self saveTLKMaterialToKeychain:self.healthZoneID];
[self saveTLKMaterialToKeychain:self.applepayZoneID];
[self saveTLKMaterialToKeychain:self.homeZoneID];
[self saveTLKMaterialToKeychain:self.limitedZoneID];
[self saveTLKMaterialToKeychain:self.passwordsZoneID];
}
- (void)deleteTLKMaterialsFromKeychain{
[self deleteTLKMaterialFromKeychain: self.engramZoneID];
[self deleteTLKMaterialFromKeychain: self.manateeZoneID];
[self deleteTLKMaterialFromKeychain: self.autoUnlockZoneID];
[self deleteTLKMaterialFromKeychain: self.healthZoneID];
[self deleteTLKMaterialFromKeychain: self.applepayZoneID];
[self deleteTLKMaterialFromKeychain: self.homeZoneID];
[self deleteTLKMaterialFromKeychain:self.limitedZoneID];
[self deleteTLKMaterialFromKeychain:self.passwordsZoneID];
}
- (void)waitForKeyHierarchyReadinesses {
[self.manateeView waitForKeyHierarchyReadiness];
[self.engramView waitForKeyHierarchyReadiness];
[self.autoUnlockView waitForKeyHierarchyReadiness];
[self.healthView waitForKeyHierarchyReadiness];
[self.applepayView waitForKeyHierarchyReadiness];
[self.homeView waitForKeyHierarchyReadiness];
[self.limitedView waitForKeyHierarchyReadiness];
[self.passwordsView waitForKeyHierarchyReadiness];
}
- (void)expectCKKSTLKSelfShareUploads {
for(CKRecordZoneID* zoneID in self.ckksZones) {
[self expectCKKSTLKSelfShareUpload:zoneID];
}
}
@end
@interface CloudKitKeychainSyncingMultiZoneTests : CloudKitKeychainSyncingMultiZoneTestsBase
@end
@implementation CloudKitKeychainSyncingMultiZoneTests
- (void)testAllViewsMakeNewKeyHierarchies {
// Test starts with nothing anywhere
// Due to our new cross-zone fetch system, CKKS should only issue one fetch for all zones
// Since the tests can sometimes be slow, slow down the fetcher to normal speed
[self.injectedManager.zoneChangeFetcher.fetchScheduler changeDelays:2*NSEC_PER_SEC continuingDelay:30*NSEC_PER_SEC];
self.silentFetchesAllowed = false;
[self expectCKFetch];
[self startCKKSSubsystem];
[self performOctagonTLKUpload:self.ckksViews];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
for(CKKSKeychainView* view in self.ckksViews) {
XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should enter 'ready' for view }
}
- (void)testAllViewsAcceptExistingKeyHierarchies {
for(CKRecordZoneID* zoneID in self.ckksZones) {
[self putFakeKeyHierarchyInCloudKit:zoneID];
[self saveTLKMaterialToKeychain:zoneID];
[self expectCKKSTLKSelfShareUpload:zoneID];
}
[self.injectedManager.zoneChangeFetcher.fetchScheduler changeDelays:2*NSEC_PER_SEC continuingDelay:30*NSEC_PER_SEC];
self.silentFetchesAllowed = false;
[self expectCKFetch];
[self startCKKSSubsystem];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
for(CKKSKeychainView* view in self.ckksViews) {
XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should enter 'ready' for view }
}
- (void)testAddEngramManateeItems {
[self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
[self startCKKSSubsystem];
XCTestExpectation* engramChanged = [self expectChangeForView:self.engramZoneID.zoneName];
XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
XCTestExpectation* manateeChanged = [self expectChangeForView:self.manateeZoneID.zoneName];
// We expect a single record to be uploaded to the engram view.
[self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.engramZoneID];
[self addGenericPassword: @"data" account: @"account-delete-me-engram" viewHint:(NSString*) kSecAttrViewHintEngram];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self waitForExpectations:@[engramChanged] timeout:1];
[self waitForExpectations:@[pcsChanged] timeout:1];
pcsChanged = [self expectChangeForView:@"PCS"];
// We expect a single record to be uploaded to the manatee view.
[self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.manateeZoneID];
[self addGenericPassword: @"data" account: @"account-delete-me-manatee" viewHint:(NSString*) kSecAttrViewHintManatee];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self waitForExpectations:@[manateeChanged] timeout:1];
[self waitForExpectations:@[pcsChanged] timeout:1];
}
- (void)testAddAutoUnlockItems {
[self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
[self startCKKSSubsystem];
XCTestExpectation* autoUnlockChanged = [self expectChangeForView:self.autoUnlockZoneID.zoneName];
// AutoUnlock is NOT is PCS view, so it should not send the fake 'PCS' view notification
XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
pcsChanged.inverted = YES;
// We expect a single record to be uploaded to the AutoUnlock view.
[self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.autoUnlockZoneID];
[self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintAutoUnlock];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self waitForExpectations:@[autoUnlockChanged] timeout:1];
[self waitForExpectations:@[pcsChanged] timeout:0.2];
}
- (void)testAddHealthItems {
[self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
[self startCKKSSubsystem];
XCTestExpectation* healthChanged = [self expectChangeForView:self.healthZoneID.zoneName];
// Health is NOT is PCS view, so it should not send the fake 'PCS' view notification
XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
pcsChanged.inverted = YES;
// We expect a single record to be uploaded to the Health view.
[self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.healthZoneID];
[self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintHealth];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self waitForExpectations:@[healthChanged] timeout:1];
[self waitForExpectations:@[pcsChanged] timeout:0.2];
}
- (void)testAddApplePayItems {
[self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
[self startCKKSSubsystem];
XCTestExpectation* applepayChanged = [self expectChangeForView:self.applepayZoneID.zoneName];
XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
// We expect a single record to be uploaded to the ApplePay view.
[self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.applepayZoneID];
[self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintApplePay];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self waitForExpectations:@[applepayChanged] timeout:1];
[self waitForExpectations:@[pcsChanged] timeout:0.2];
}
- (void)testAddHomeItems {
[self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
[self startCKKSSubsystem];
XCTestExpectation* homeChanged = [self expectChangeForView:self.homeZoneID.zoneName];
// Home is a now PCS view, so it should send the fake 'PCS' view notification
XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
// We expect a single record to be uploaded to the ApplePay view.
[self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.homeZoneID];
[self addGenericPassword: @"data" account: @"account-delete-me-autounlock" viewHint:(NSString*) kSecAttrViewHintHome];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self waitForExpectations:@[homeChanged] timeout:1];
[self waitForExpectations:@[pcsChanged] timeout:0.2];
}
- (void)testAddLimitedPeersAllowedItems {
[self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
[self startCKKSSubsystem];
XCTestExpectation* limitedChanged = [self expectChangeForView:self.limitedZoneID.zoneName];
// LimitedPeersAllowed is a PCS view, so it should send the fake 'PCS' view notification
XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
// We expect a single record to be uploaded to the LimitedPeersOkay view.
[self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.limitedZoneID];
[self addGenericPassword: @"data" account: @"account-delete-me-limited-peers" viewHint:(NSString*) kSecAttrViewHintLimitedPeersAllowed];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self waitForExpectations:@[limitedChanged] timeout:1];
[self waitForExpectations:@[pcsChanged] timeout:0.2];
}
- (void)testMultipleZoneAdd {
[self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
// Let the horses loose
[self startCKKSSubsystem];
// We expect a single record to be uploaded to the 'LimitedPeersAllowed' view
[self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.limitedZoneID];
[self addGenericPassword: @"data" account: @"account-delete-me-limited-peers" viewHint:(NSString*)kSecAttrViewHintLimitedPeersAllowed];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
// We expect a single record to be uploaded to the 'atv' home
[self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.homeZoneID];
[self addGenericPassword: @"data" account: @"account-delete-me-home" viewHint:(NSString*)kSecAttrViewHintHome];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
OCMVerifyAllWithDelay(self.mockCKKSViewManager, 10);
}
- (void)testMultipleZoneDelete {
[self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
[self startCKKSSubsystem];
// We expect a single record to be uploaded.
[self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.limitedZoneID];
[self addGenericPassword: @"data" account: @"account-delete-me-limited-peers" viewHint:(NSString*)kSecAttrViewHintLimitedPeersAllowed];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.homeZoneID];
[self addGenericPassword: @"data" account: @"account-delete-me-home" viewHint:(NSString*)kSecAttrViewHintHome];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self waitForCKModifications];
// We expect a single record to be deleted from the ATV zone
[self expectCKDeleteItemRecords:1 zoneID:self.homeZoneID];
[self deleteGenericPassword:@"account-delete-me-home"];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
// Now we expect a single record to be deleted from the test zone
[self expectCKDeleteItemRecords:1 zoneID:self.limitedZoneID];
[self deleteGenericPassword:@"account-delete-me-limited-peers"];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
}
- (void)testAddAndReceiveDeleteForSafariPasswordItem {
[self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
[self startCKKSSubsystem];
XCTestExpectation* passwordChanged = [self expectChangeForView:self.passwordsView.zoneName];
// We expect a single record to be uploaded to the Passwords view.
[self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.passwordsZoneID];
[self addGenericPassword:@"data"
account:@"account-delete-me"
access:(id)kSecAttrAccessibleWhenUnlocked
viewHint:nil
accessGroup:@"com.apple.sbd"
expecting:errSecSuccess
message:@"Item for Password view"];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self waitForExpectations:@[passwordChanged] timeout:1];
[self waitForCKModifications];
[self waitForKeyHierarchyReadinesses];
[self.passwordsView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
// Ensure that we catch up to the newest CK change token so our fake cloudkit will notice the delete at fetch time
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
[self.passwordsView waitForFetchAndIncomingQueueProcessing];
// Now, the item is deleted. Do we properly remove it?
CKRecord* itemRecord = nil;
for(CKRecord* record in [self.passwordsZone.currentDatabase allValues]) {
if([record.recordType isEqualToString:SecCKRecordItemType]) {
itemRecord = record;
break;
}
}
XCTAssertNotNil(itemRecord, "Should have found the item in the password zone");
NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecAttrAccessGroup : @"com.apple.sbd",
(id)kSecAttrAccount : @"account-delete-me",
(id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
(id)kSecMatchLimit : (id)kSecMatchLimitOne,
(id)kSecReturnData : (id)kCFBooleanTrue,
};
CFTypeRef item = NULL;
XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should still exist");
XCTAssertNotNil((__bridge id)item, "An item should have been found");
CFReleaseNull(item);
// Now, the item is deleted. The passwords view should delete the local item, even though it has the wrong 'vwht' on disk.
XCTAssertNotNil(self.passwordsZone.currentDatabase[itemRecord.recordID], "Record should exist in fake CK");
[self.passwordsZone deleteCKRecordIDFromZone:itemRecord.recordID];
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
[self.passwordsView waitForFetchAndIncomingQueueProcessing];
XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should no longer exist");
XCTAssertNil((__bridge id)item, "No item should have been found");
}
- (void)testAddOtherViewHintItem {
[self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
[self startCKKSSubsystem];
// We expect no uploads to CKKS.
[self addGenericPassword: @"data" account: @"account-delete-me-no-viewhint"];
[self addGenericPassword: @"data" account: @"account-delete-me-password" viewHint:(NSString*) kSOSViewBackupBagV0];
sleep(1);
OCMVerifyAllWithDelay(self.mockDatabase, 20);
}
- (void)testUploadItemsAddedBeforeStart {
[self addGenericPassword:@"data"
account:@"account-delete-me"
access:(id)kSecAttrAccessibleAfterFirstUnlock
viewHint:nil
accessGroup:@"com.apple.sbd"
expecting:errSecSuccess
message:@"Item for Password view"];
[self addGenericPassword:@"data"
account:@"account-delete-me-2"
access:(id)kSecAttrAccessibleAfterFirstUnlock
viewHint:nil
accessGroup:@"com.apple.sbd"
expecting:errSecSuccess
message:@"Item for Password view"];
[self addGenericPassword:@"data" account:@"account-delete-me-limited-peers" viewHint:(NSString*)kSecAttrViewHintLimitedPeersAllowed];
NSError* error = nil;
NSDictionary* currentOQEs = [CKKSOutgoingQueueEntry countsByStateInZone:self.passwordsZoneID error:&error];
XCTAssertNil(error, "Should be no error counting OQEs");
XCTAssertEqual(0, currentOQEs.count, "Should be no OQEs");
[self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
// Now CKKS starts up
// Upon sign in, these items should be uploaded
[self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.passwordsZoneID
checkItem:[self checkClassCBlock:self.passwordsZoneID message:@"Object was encrypted under class C key in hierarchy"]];
[self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.limitedZoneID];
[self startCKKSSubsystem];
XCTAssertEqual(0, [self.passwordsView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS entered ready");
OCMVerifyAllWithDelay(self.mockDatabase, 20);
}
- (void)testReceiveItemInView {
[self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
[self startCKKSSubsystem];
[self waitForKeyHierarchyReadinesses];
[self.engramView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
[self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
CKRecord* ckr = [self createFakeRecord:self.engramZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
[self.engramZone addToZone: ckr];
XCTestExpectation* engramChanged = [self expectChangeForView:self.engramZoneID.zoneName];
XCTestExpectation* pcsChanged = [self expectChangeForView:@"PCS"];
self.silentFetchesAllowed = false;
[self expectCKFetch];
// Trigger a notification (with hilariously fake data)
[self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self.engramView waitForFetchAndIncomingQueueProcessing];
[self findGenericPassword:@"account-delete-me" expecting:errSecSuccess];
[self waitForExpectations:@[engramChanged] timeout:1];
[self waitForExpectations:@[pcsChanged] timeout:1];
}
- (void)testRecoverFromCloudKitOldChangeTokenInKeyHierarchyFetch {
[self putFakeKeyHierachiesInCloudKit];
[self saveTLKsToKeychain];
[self expectCKKSTLKSelfShareUploads];
// Spin up CKKS subsystem.
[self startCKKSSubsystem];
[self waitForKeyHierarchyReadinesses];
// We expect a single record to be uploaded
[self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
[self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
// Delete all old database states, to destroy the change tag validity
[self.manateeZone.pastDatabases removeAllObjects];
// We expect a total local flush and refetch
self.silentFetchesAllowed = false;
[self expectCKFetch]; // one to fail with a CKErrorChangeTokenExpired error
[self expectCKFetch]; // and one to succeed
[self.manateeView.stateMachine handleFlag:CKKSFlagFetchRequested];
XCTAssertEqual(0, [self.manateeView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "CKKS should enter 'ready'");
// Don't cause another fetch, because the machinery might not be ready
[self.manateeView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
// And check that a new upload happens just fine.
[self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.manateeZoneID checkItem: [self checkClassABlock:self.manateeZoneID message:@"Object was encrypted under class A key in hierarchy"]];
[self addGenericPassword:@"asdf"
account:@"account-class-A"
viewHint:(id)kSecAttrViewHintManatee
access:(id)kSecAttrAccessibleWhenUnlocked
expecting:errSecSuccess
message:@"Adding class A item"];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
}
- (void)testResetAllCloudKitZones {
self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
OCMExpect([self.suggestTLKUpload trigger]);
[self putFakeKeyHierachiesInCloudKit];
[self saveTLKsToKeychain];
[self expectCKKSTLKSelfShareUploads];
// Spin up CKKS subsystem.
[self startCKKSSubsystem];
[self waitForKeyHierarchyReadinesses];
// We expect a single record to be uploaded
[self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
[self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self waitForCKModifications];
// During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
[self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
// CKKS should issue exactly one deletion for all of these
self.silentZoneDeletesAllowed = true;
XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
[self.injectedManager rpcResetCloudKit:nil reason:@"reset-all-test" reply:^(NSError* result) {
XCTAssertNil(result, "no error resetting cloudkit");
ckksnotice_global("ckks", "Received a resetCloudKit callback");
[resetExpectation fulfill];
}];
// Sneak in and perform Octagon's duties
OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
[self performOctagonTLKUpload:self.ckksViews];
[self waitForExpectations:@[resetExpectation] timeout:20];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem: [self checkClassABlock:self.manateeZoneID message:@"Object was encrypted under class A key in hierarchy"]];
[self addGenericPassword:@"asdf"
account:@"account-class-A"
viewHint:(id)kSecAttrViewHintManatee
access:(id)kSecAttrAccessibleWhenUnlocked
expecting:errSecSuccess
message:@"Adding class A item"];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
}
- (void)testResetAllCloudKitZonesWithPartialZonesMissing {
self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
OCMExpect([self.suggestTLKUpload trigger]);
[self putFakeKeyHierachiesInCloudKit];
[self saveTLKsToKeychain];
[self expectCKKSTLKSelfShareUploads];
// Spin up CKKS subsystem.
[self startCKKSSubsystem];
[self waitForKeyHierarchyReadinesses];
// We expect a single record to be uploaded
[self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
[self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self waitForCKModifications];
self.zones[self.manateeZoneID] = nil;
self.keys[self.manateeZoneID] = nil;
self.zones[self.applepayZoneID] = nil;
self.keys[self.applepayZoneID] = nil;
// During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
[self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
// CKKS should issue exactly one deletion for all of these
self.silentZoneDeletesAllowed = true;
XCTestExpectation* resetExpectation = [self expectationWithDescription: @"reset callback occurs"];
[self.injectedManager rpcResetCloudKit:nil reason:@"reset-all-test" reply:^(NSError* result) {
XCTAssertNil(result, "no error resetting cloudkit");
ckksnotice_global("ckks", "Received a resetCloudKit callback");
[resetExpectation fulfill];
}];
// Sneak in and perform Octagon's duties
OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
[self performOctagonTLKUpload:self.ckksViews];
[self waitForExpectations:@[resetExpectation] timeout:20];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem: [self checkClassABlock:self.manateeZoneID message:@"Object was encrypted under class A key in hierarchy"]];
[self addGenericPassword:@"asdf"
account:@"account-class-A"
viewHint:(id)kSecAttrViewHintManatee
access:(id)kSecAttrAccessibleWhenUnlocked
expecting:errSecSuccess
message:@"Adding class A item"];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
}
- (void)testResetMultiCloudKitZoneCloudKitRejects {
self.suggestTLKUpload = OCMClassMock([CKKSNearFutureScheduler class]);
OCMExpect([self.suggestTLKUpload trigger]);
[self putFakeKeyHierachiesInCloudKit];
[self saveTLKsToKeychain];
[self expectCKKSTLKSelfShareUploads];
// Spin up CKKS subsystem.
[self startCKKSSubsystem];
[self waitForKeyHierarchyReadinesses];
// We expect a single record to be uploaded
[self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID message:@"Object was encrypted under class C key in hierarchy"]];
[self addGenericPassword: @"data" account: @"account-delete-me" viewHint:(id)kSecAttrViewHintManatee];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self waitForCKModifications];
self.nextModifyRecordZonesError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
code:CKErrorZoneBusy
userInfo:@{
CKErrorRetryAfterKey: @(0.2),
NSUnderlyingErrorKey: [[CKPrettyError alloc] initWithDomain:CKErrorDomain
code:2029
userInfo:nil],
}];
self.silentZoneDeletesAllowed = true;
// During the reset, Octagon will upload the key hierarchy, and then CKKS will upload the class C item
[self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.manateeZoneID checkItem:[self checkClassCBlock:self.manateeZoneID 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");
ckksnotice_global("ckks", "Received a resetCloudKit callback");
[resetExpectation fulfill];
}];
// Sneak in and perform Octagon's duties
OCMVerifyAllWithDelay(self.suggestTLKUpload, 10);
[self performOctagonTLKUpload:self.ckksViews];
[self waitForExpectations:@[resetExpectation] timeout:20];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
XCTAssertNil(self.nextModifyRecordZonesError, "zone modification error should have been cleared");
}
- (void)testMultiZoneDeviceStateUploadGood {
[self putFakeKeyHierachiesInCloudKit];
[self saveTLKsToKeychain];
[self expectCKKSTLKSelfShareUploads];
// Spin up CKKS subsystem.
[self startCKKSSubsystem];
[self waitForKeyHierarchyReadinesses];
for(CKKSKeychainView* view in self.ckksViews) {
[self expectCKModifyRecords:@{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
deletedRecordTypeCounts:nil
zoneID:view.zoneID
checkModifiedRecord:nil
runAfterModification:nil];
}
[self.injectedManager xpc24HrNotification];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
}
- (void)testMultiZoneResync {
// Set up
[self putFakeKeyHierachiesInCloudKit];
[self saveTLKsToKeychain];
[self expectCKKSTLKSelfShareUploads];
// Put sample data in zones, and save off a change token for later fetch shenanigans
[self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D00" withAccount:@"manatee0"]];
[self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D01" withAccount:@"manatee1"]];
CKServerChangeToken* manateeChangeToken1 = self.manateeZone.currentChangeToken;
[self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D02" withAccount:@"manatee2"]];
[self.manateeZone addToZone:[self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-0000-0000-0000-5A507ACB2D03" withAccount:@"manatee3"]];
[self.healthZone addToZone:[self createFakeRecord:self.healthZoneID recordName:@"7B598D31-0000-0000-FFFF-5A507ACB2D00" withAccount:@"health0"]];
[self.healthZone addToZone:[self createFakeRecord:self.healthZoneID recordName:@"7B598D31-0000-0000-FFFF-5A507ACB2D01" withAccount:@"health1"]];
[self startCKKSSubsystem];
[self waitForKeyHierarchyReadinesses];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self.manateeView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
[self.healthView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
[self findGenericPassword:@"manatee0" expecting:errSecSuccess];
[self findGenericPassword:@"manatee1" expecting:errSecSuccess];
[self findGenericPassword:@"manatee2" expecting:errSecSuccess];
[self findGenericPassword:@"manatee3" expecting:errSecSuccess];
[self findGenericPassword:@"health0" expecting:errSecSuccess];
[self findGenericPassword:@"health1" expecting:errSecSuccess];
// Now, we resync. But, the manatee zone comes down in two fetches
self.silentFetchesAllowed = false;
[self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
// Assert that the fetch is a refetch
CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.manateeZoneID].previousServerChangeToken;
if(changeToken == nil) {
return YES;
} else {
return NO;
}
} runBeforeFinished:^{}];
[self expectCKFetchWithFilter:^BOOL(FakeCKFetchRecordZoneChangesOperation * _Nonnull frzco) {
// Assert that the fetch is happening with the change token we paused at before
CKServerChangeToken* changeToken = frzco.configurationsByRecordZoneID[self.manateeZoneID].previousServerChangeToken;
if(changeToken && [changeToken isEqual:manateeChangeToken1]) {
return YES;
} else {
return NO;
}
} runBeforeFinished:^{}];
self.manateeZone.limitFetchTo = manateeChangeToken1;
// Attempt to trigger simultaneous key state resyncs. This is a horrible hack...
[self.manateeView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
self.manateeView.keyStateFullRefetchRequested = YES;
[self.manateeView _onqueuePokeKeyStateMachine];
return CKKSDatabaseTransactionCommit;
}];
[self.healthView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
self.healthView.keyStateFullRefetchRequested = YES;
[self.healthView _onqueuePokeKeyStateMachine];
return CKKSDatabaseTransactionCommit;
}];
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self.manateeView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
[self.healthView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
// And all items should still exist
[self findGenericPassword:@"manatee0" expecting:errSecSuccess];
[self findGenericPassword:@"manatee1" expecting:errSecSuccess];
[self findGenericPassword:@"manatee2" expecting:errSecSuccess];
[self findGenericPassword:@"manatee3" expecting:errSecSuccess];
[self findGenericPassword:@"health0" expecting:errSecSuccess];
[self findGenericPassword:@"health1" expecting:errSecSuccess];
}
@end
#endif // OCTAGON