CKKSTests+ItemSyncChoice.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 "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 CloudKitKeychainSyncingItemSyncChoiceTests : CloudKitKeychainSyncingTestsBase
@property CKKSSOSSelfPeer* remotePeer1;
@end

@implementation CloudKitKeychainSyncingItemSyncChoiceTests

- (size_t)outgoingQueueSize:(CKKSKeychainView*)view {
    __block size_t result = 0;

    [view dispatchSyncWithReadOnlySQLTransaction:^{
        NSError* zoneError = nil;
        NSArray<CKKSOutgoingQueueEntry*>* entries = [CKKSOutgoingQueueEntry all:view.zoneID error:&zoneError];
        XCTAssertNil(zoneError, "should be no error fetching all OQEs");

        result = (size_t)entries.count;
    }];
    return result;
}

- (size_t)incomingQueueSize:(CKKSKeychainView*)view {
    __block size_t result = 0;

    [view dispatchSyncWithReadOnlySQLTransaction:^{
        NSError* zoneError = nil;
        NSArray<CKKSIncomingQueueEntry*>* entries = [CKKSIncomingQueueEntry all:view.zoneID error:&zoneError];
        XCTAssertNil(zoneError, "should be no error fetching all IQEs");

        result = (size_t)entries.count;
    }];
    return result;
}

- (void)setUp {
    [super setUp];

    self.remotePeer1 = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"remote-peer1"
                                                    encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
                                                       signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
                                                         viewList:self.managedViewList];
}

- (void)testAddItemToPausedView {
    [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.

    [self startCKKSSubsystem];
    XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");

    [self.keychainView setCurrentSyncingPolicy:[self viewSortingPolicyForManagedViewListWithUserControllableViews:[NSSet setWithObject:self.keychainView.zoneName]
                                                                                        syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_DISABLED]
                                 policyIsFresh:NO];

    [self addGenericPassword:@"data" account:@"account-delete-me"];
    [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
    XCTAssertEqual(1, [self outgoingQueueSize:self.keychainView], "There should be one pending item in the outgoing queue");

    // and again
    [self addGenericPassword:@"data" account:@"account-delete-me-2"];
    [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
    XCTAssertEqual(2, [self outgoingQueueSize:self.keychainView], "There should be two pending item in the outgoing queue");

    // When syncing is enabled, these items should sync
    [self expectCKModifyItemRecords:2 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];

    [self.keychainView setCurrentSyncingPolicy:[self viewSortingPolicyForManagedViewListWithUserControllableViews:[NSSet setWithObject:self.keychainView.zoneName]
                                                                                        syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_ENABLED]
                                 policyIsFresh:NO];
    OCMVerifyAllWithDelay(self.mockDatabase, 20);
}

- (void)testReceiveItemToPausedView {
    [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.

    [self startCKKSSubsystem];
    XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should have arrived at ready");

    [self.keychainView setCurrentSyncingPolicy:[self viewSortingPolicyForManagedViewListWithUserControllableViews:[NSSet setWithObject:self.keychainView.zoneName]
                                                                                        syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_DISABLED]
                                 policyIsFresh:NO];

    [self findGenericPassword: @"account0" expecting:errSecItemNotFound];

    [self.keychainZone addToZone:[self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D00" withAccount:@"account0"]];
    [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
    [self.keychainView waitForFetchAndIncomingQueueProcessing];
    XCTAssertEqual(1, [self incomingQueueSize:self.keychainView], "There should be one pending item in the incoming queue");

    [self.keychainZone addToZone:[self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-0000-5A507ACB2D00" withAccount:@"account1"]];
    [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
    [self.keychainView waitForFetchAndIncomingQueueProcessing];
    XCTAssertEqual(2, [self incomingQueueSize:self.keychainView], "There should be two pending item in the incoming queue");

    [self findGenericPassword:@"account0" expecting:errSecItemNotFound];
    [self findGenericPassword:@"account1" expecting:errSecItemNotFound];

    // When syncing is enabled, these items should sync
    [self.keychainView setCurrentSyncingPolicy:[self viewSortingPolicyForManagedViewListWithUserControllableViews:[NSSet setWithObject:self.keychainView.zoneName]
                                                                                        syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_ENABLED]
                                 policyIsFresh:NO];

    [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
    [self findGenericPassword:@"account0" expecting:errSecSuccess];
    [self findGenericPassword:@"account1" expecting:errSecSuccess];
}

- (void)testAcceptKeyHierarchyWhilePaused {
    [self.keychainView setCurrentSyncingPolicy:[self viewSortingPolicyForManagedViewListWithUserControllableViews:[NSSet setWithObject:self.keychainView.zoneName]
                                                                                        syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_DISABLED]
                                 policyIsFresh:NO];

    [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], "Key state should have become ready");
}

- (void)testUploadSelfTLKShare {
    [self.keychainView setCurrentSyncingPolicy:[self viewSortingPolicyForManagedViewListWithUserControllableViews:[NSSet setWithObject:self.keychainView.zoneName]
                                                                                        syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_DISABLED]
                                 policyIsFresh:NO];

    // Test starts with no keys in CKKS database, but one in our fake CloudKit.
    [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];

    // Test also starts with the TLK shared to all trusted peers from peer1
    [self.mockSOSAdapter.trustedPeers addObject:self.remotePeer1];
    [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];

    // The CKKS subsystem should accept the keys, and share the TLK back to itself
    [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
    [self startCKKSSubsystem];
    XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");

    OCMVerifyAllWithDelay(self.mockDatabase, 20);
}

- (void)testSendNewTLKSharesOnTrustSetAddition {
    [self.keychainView setCurrentSyncingPolicy:[self viewSortingPolicyForManagedViewListWithUserControllableViews:[NSSet setWithObject:self.keychainView.zoneName]
                                                                                        syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_DISABLED]
                                 policyIsFresh:NO];

    // step 1: add a new peer; we should share the TLK with them
    // start with no trusted peers
    [self.mockSOSAdapter.trustedPeers removeAllObjects];

    [self startCKKSSubsystem];
    [self performOctagonTLKUpload:self.ckksViews];

    [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
    [self.mockSOSAdapter.trustedPeers addObject:self.remotePeer1];
    [self.mockSOSAdapter sendTrustedPeerSetChangedUpdate];

    OCMVerifyAllWithDelay(self.mockDatabase, 20);
    [self waitForCKModifications];

    // and just double-check that no syncing is occurring
    [self addGenericPassword:@"data" account:@"account-delete-me"];
    [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
    XCTAssertEqual(1, [self outgoingQueueSize:self.keychainView], "There should be one pending item in the outgoing queue");
}

- (void)testAddAndNotifyOnSyncDuringPausedOperation {
    [self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
    [self startCKKSSubsystem];

    XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should have become ready");
    OCMVerifyAllWithDelay(self.mockDatabase, 20);

    [self.keychainView setCurrentSyncingPolicy:[self viewSortingPolicyForManagedViewListWithUserControllableViews:[NSSet setWithObject:self.keychainView.zoneName]
                                                                                        syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_DISABLED]
                                 policyIsFresh:NO];

    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 cferror) {
        XCTAssertFalse(didSync, "Item did not sync");

        NSError* error = (__bridge NSError*)cferror;
        XCTAssertNotNil(error, "Error syncing item");
        XCTAssertEqual(error.domain, CKKSErrorDomain, "Error domain was CKKSErrorDomain");
        XCTAssertEqual(error.code, CKKSErrorViewIsPaused, "Error code is 'view is paused'");

        [blockExpectation fulfill];
    }), @"_SecItemAddAndNotifyOnSync succeeded");

    OCMVerifyAllWithDelay(self.mockDatabase, 10);

    [self waitForExpectationsWithTimeout:5.0 handler:nil];
}

@end

#endif // OCTAGON