CKKSSOSTests.m   [plain text]


/*
 * Copyright (c) 2017 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 <notify.h>

#include <Security/SecItemPriv.h>

#import "keychain/ckks/tests/CloudKitMockXCTest.h"
#import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
#import "keychain/ckks/tests/CKKSTests+MultiZone.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/CKKSSynchronizeOperation.h"
#import "keychain/ckks/CKKSViewManager.h"
#import "keychain/ckks/CKKSZoneStateEntry.h"
#import "keychain/ckks/CKKSManifest.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 CloudKitKeychainSyncingSOSIntegrationTests : CloudKitKeychainSyncingMultiZoneTestsBase
@end

@implementation CloudKitKeychainSyncingSOSIntegrationTests

- (void)testFindManateePiggyTLKs {
    [self saveFakeKeyHierarchyToLocalDatabase:self.manateeZoneID];
    [self saveTLKMaterialToKeychain:self.manateeZoneID];

    [self startCKKSSubsystem];

    NSDictionary* piggyTLKs = [self SOSPiggyBackCopyFromKeychain];

    [self deleteTLKMaterialFromKeychain:self.manateeZoneID];

    [self SOSPiggyBackAddToKeychain:piggyTLKs];

    NSError* error = nil;
    [self.manateeZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
    XCTAssertNil(error, "No error loading tlk from piggy contents");
}

- (void)testFindPiggyTLKs {
    [self putFakeKeyHierachiesInCloudKit];
    [self putFakeDeviceStatusesInCloudKit];
    [self saveTLKsToKeychain];

    [self startCKKSSubsystem];

    NSDictionary* piggyTLKs = [self SOSPiggyBackCopyFromKeychain];

    [self deleteTLKMaterialsFromKeychain];

    [self SOSPiggyBackAddToKeychain:piggyTLKs];

    NSError* error = nil;
    [self.manateeZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
    XCTAssertNil(error, "No error loading manatee tlk from piggy contents");

    [self.engramZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
    XCTAssertNil(error, "No error loading engram tlk from piggy contents");

    [self.autoUnlockZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
    XCTAssertNil(error, "No error loading AutoUnlock tlk from piggy contents");

    [self.healthZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
    XCTAssertNil(error, "No error loading Health tlk from piggy contents");

    [self.applepayZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
    XCTAssertNil(error, "No error loading ApplePay tlk from piggy contents");

    [self.homeZoneKeys.tlk loadKeyMaterialFromKeychain:&error];
    XCTAssertNil(error, "No error loading Home tlk from piggy contents");
}

-(void)testPiggybackingData{
    [self putFakeKeyHierachiesInCloudKit];
    [self saveTLKsToKeychain];

    for(CKRecordZoneID* zoneID in self.ckksZones) {
        [self expectCKKSTLKSelfShareUpload:zoneID];
    }
    [self startCKKSSubsystem];

    [self waitForKeyHierarchyReadinesses];

    OCMVerifyAllWithDelay(self.mockDatabase, 20);

    /*
     * Pull data from keychain and view manager
     */

    NSDictionary* piggydata = [self SOSPiggyBackCopyFromKeychain];
    NSArray<NSData *>* icloudidentities = piggydata[@"idents"];
    NSArray<NSDictionary *>* tlks = piggydata[@"tlk"];

    XCTAssertEqual([tlks count], [[self.injectedManager viewList] count], "TLKs not same as views");

    XCTAssertNotNil(tlks, "tlks not set");
    XCTAssertNotEqual([tlks count], (NSUInteger)0, "0 tlks");
    XCTAssertNotNil(icloudidentities, "idents not set");
    XCTAssertNotEqual([icloudidentities count], (NSUInteger)0, "0 icloudidentities");

    NSData *initial = SOSPiggyCreateInitialSyncData(icloudidentities, tlks);

    XCTAssertNotNil(initial, "Initial not set");
    XCTAssertNotEqual((int)[initial length], 0, "initial sync data is greater than 0");

    /*
     * Check that they make it accross
     */

    const uint8_t* der = [initial bytes];
    const uint8_t *der_end = der + [initial length];

    NSDictionary *result = SOSPiggyCopyInitialSyncData(&der, der_end);
    XCTAssertNotNil(result, "Initial not set");
    NSArray *copiedTLKs = result[@"tlks"];
    XCTAssertNotNil(copiedTLKs, "tlks not set");
    XCTAssertEqual([copiedTLKs count], 5u, "piggybacking should have gotten 5 TLKs across (but we have more than that elsewhere)");

    NSArray *copiediCloudidentities = result[@"idents"];
    XCTAssertNotNil(copiediCloudidentities, "idents not set");
    XCTAssertEqual([copiediCloudidentities count], [icloudidentities count], "ident count not same");
}


- (void)testPiggybackingTLKRequest {
    [self putFakeKeyHierachiesInCloudKit];
    [self saveTLKsToKeychain];

    for(CKRecordZoneID* zoneID in self.ckksZones) {
        [self expectCKKSTLKSelfShareUpload:zoneID];
    }
    [self startCKKSSubsystem];
    [self waitForKeyHierarchyReadinesses];
    OCMVerifyAllWithDelay(self.mockDatabase, 20);

    // The "tlk request" piggybacking session calls SOSAccountCopyInitialSyncData
    __block CFErrorRef cferror = NULL;
    NSData* piggybackingData = (NSData*) CFBridgingRelease(SOSAccountCopyInitialSyncData(nil, kSOSInitialSyncFlagTLKsRequestOnly, &cferror));

    XCTAssertEqual(cferror, NULL, "Should have no error fetching only the TLKs");
    XCTAssertNotNil(piggybackingData, "Should have received some sync data");

    const uint8_t* der = [piggybackingData bytes];
    const uint8_t *der_end = der + [piggybackingData length];

    NSDictionary *result = SOSPiggyCopyInitialSyncData(&der, der_end);
    XCTAssertNotNil(result, "Should be able to parse the piggybacking data");

    NSArray *copiedTLKs = result[@"tlks"];
    XCTAssertNotNil(copiedTLKs, "should have some tlks");
    XCTAssertEqual([copiedTLKs count], 1u, "piggybacking should have gotten 1 TLK");
    XCTAssertEqualObjects(copiedTLKs[0][@"srvr"], @"Passwords", "should have the passwords TLK only");
    NSData* keyData = copiedTLKs[0][@"v_Data"];
    XCTAssertNotNil(keyData, "Should have some key material");
    XCTAssertEqual([keyData length], 64, "Key material should be 64 bytes");

    NSArray *copiediCloudidentities = result[@"idents"];
    XCTAssertNotNil(copiediCloudidentities, "idents not set");
    XCTAssertEqual([copiediCloudidentities count], 0, "Should have no icloud identities");
}

-(void)testVerifyTLKSorting {
    char key[32*2] = {0};
    NSArray<NSDictionary *> *tlks = @[
        @{
            @"acct" : @"11111111",
            @"srvr" : @"Manatee",
            @"v_Data" : [NSData dataWithBytes:key length:sizeof(key)],
            @"auth" : @YES,
        },
        @{
            @"acct" : @"55555555",
            @"srvr" : @"Health",
            @"v_Data" : [NSData dataWithBytes:key length:sizeof(key)],
        },
        @{
            @"acct" : @"22222222",
            @"srvr" : @"Engram",
            @"v_Data" : [NSData dataWithBytes:key length:sizeof(key)],
            @"auth" : @YES,
        },
        @{
            @"acct" : @"44444444",
            @"srvr" : @"Manatee",
            @"v_Data" : [NSData dataWithBytes:key length:sizeof(key)],
        },
        @{
            @"acct" : @"33333333",
            @"srvr" : @"Health",
            @"v_Data" : [NSData dataWithBytes:key length:sizeof(key)],
            @"auth" : @YES,
        },
        @{
            @"acct" : @"66666666",
            @"srvr" : @"Home",
            @"v_Data" : [NSData dataWithBytes:key length:sizeof(key)],
            @"auth" : @YES,
        },
    ];

    NSArray<NSDictionary *>* sortedTLKs = SOSAccountSortTLKS(tlks);
    XCTAssertNotNil(sortedTLKs, "sortedTLKs not set");

    // Home gets sorted into the middle, as the other Health and Manatee TLKs aren't 'authoritative'
    NSArray<NSString *> *expectedOrder = @[ @"11111111", @"22222222", @"33333333", @"66666666", @"44444444", @"55555555"];
    [sortedTLKs enumerateObjectsUsingBlock:^(NSDictionary *tlk, NSUInteger idx, BOOL * _Nonnull stop) {
        NSString *uuid = tlk[@"acct"];
        XCTAssertEqualObjects(uuid, expectedOrder[idx], "wrong order");
    }];
}


- (void)testAcceptExistingPiggyKeyHierarchy {
    // Test starts with no keys in CKKS database, but one in our fake CloudKit.
    // Test also begins with the TLK having arrived in the local keychain (via SOS)
    [self putFakeKeyHierachiesInCloudKit];
    [self saveTLKsToKeychain];
    NSDictionary* piggyTLKS = [self SOSPiggyBackCopyFromKeychain];
    [self SOSPiggyBackAddToKeychain:piggyTLKS];
    [self deleteTLKMaterialsFromKeychain];

    // The CKKS subsystem should write a TLK Share for each view
    for(CKRecordZoneID* zoneID in self.ckksZones) {
        [self expectCKKSTLKSelfShareUpload:zoneID];
    }

    // Spin up CKKS subsystem.
    [self startCKKSSubsystem];

    [self.manateeView waitForKeyHierarchyReadiness];
    
    OCMVerifyAllWithDelay(self.mockDatabase, 20);
    
    // Verify that there are three local keys, and three local current key records
    __weak __typeof(self) weakSelf = self;
    [self.manateeView dispatchSyncWithReadOnlySQLTransaction:^{
        __strong __typeof(weakSelf) strongSelf = weakSelf;
        XCTAssertNotNil(strongSelf, "self exists");
        
        NSError* error = nil;
        
        NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.manateeZoneID error:&error];
        XCTAssertNil(error, "no error fetching keys");
        XCTAssertEqual(keys.count, 3u, "Three keys in local database");
        
        NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all:strongSelf.manateeZoneID error:&error];
        XCTAssertNil(error, "no error fetching current keys");
        XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");

        // Ensure that the manatee syncable TLK is created from a piggy
        NSDictionary* query = @{
                                (id)kSecClass : (id)kSecClassInternetPassword,
                                (id)kSecUseDataProtectionKeychain : @YES,
                                (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
                                (id)kSecAttrDescription: SecCKKSKeyClassTLK,
                                (id)kSecAttrAccount: strongSelf.manateeZoneKeys.tlk.uuid,
                                (id)kSecAttrServer: strongSelf.manateeZoneID.zoneName,
                                (id)kSecAttrSynchronizable: @YES,
                                (id)kSecReturnAttributes: @YES,
                                (id)kSecReturnData: @YES,
                                };
        CFTypeRef result = nil;
        XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef)query, &result), "Found a syncable TLK");
        XCTAssertNotNil((__bridge id) result, "Received a result from SecItemCopyMatching");
        CFReleaseNull(result);
    }];
}

-(void)testAcceptExistingAndUsePiggyKeyHierarchy {
    // Test starts with nothing in database, but one in our fake CloudKit.
    [self putFakeKeyHierachiesInCloudKit];
    [self putFakeDeviceStatusesInCloudKit];
    [self saveTLKsToKeychain];
    NSDictionary* piggyData = [self SOSPiggyBackCopyFromKeychain];
    [self deleteTLKMaterialsFromKeychain];
    
    // Spin up CKKS subsystem.
    [self startCKKSSubsystem];
    
    // The CKKS subsystem should not try to write anything to the CloudKit database.
    XCTAssertEqual(0, [self.manateeView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");

    OCMVerifyAllWithDelay(self.mockDatabase, 20);
    
    // Now, save the TLKs to the keychain (to simulate them coming in later via piggybacking).
    for(CKRecordZoneID* zoneID in self.ckksZones) {
        [self expectCKKSTLKSelfShareUpload:zoneID];
    }

    [self SOSPiggyBackAddToKeychain:piggyData];
    [self waitForKeyHierarchyReadinesses];
    
    // We expect a single record to be uploaded for each key class
    [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-manatee" viewHint:(id)kSecAttrViewHintManatee];
    
    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);
}
@end

#endif // OCTAGON