CDKeychainTests.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@
*/

#import "SecCDKeychain.h"
#import "CKKS.h"
#import "spi.h"
#import "SecItemServer.h"
#import "SecdTestKeychainUtilities.h"
#import "server_security_helpers.h"
#import "SecDbKeychainItemV7.h"
#import "KeychainXCTest.h"
#import "SecCDKeychainManagedItem+CoreDataClass.h"
#import "SecFileLocations.h"
#import <SecurityFoundation/SFEncryptionOperation.h>
#import <SecurityFoundation/SFKeychain.h>
#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>

#if USE_KEYSTORE

@interface SecCDKeychain (UnitTestingRedeclarations)

@property (readonly, getter=_queue) dispatch_queue_t queue;

- (void)_registerItemTypeForTesting:(SecCDKeychainItemType*)itemType;

- (void)performOnManagedObjectQueue:(void (^)(NSManagedObjectContext* context, NSError* error))block;
- (void)performOnManagedObjectQueueAndWait:(void (^)(NSManagedObjectContext* context, NSError* error))block;
- (SecCDKeychainManagedItem*)fetchManagedItemForPersistentID:(NSUUID*)persistentID withManagedObjectContext:(NSManagedObjectContext*)managedObjectContext error:(NSError**)error;

- (NSData*)_onQueueGetDatabaseKeyDataWithError:(NSError**)error;
- (void)_onQueueDropClassAPersistentStore;

@end

@interface SecCDKeychainItem (UnitTestingRedeclarations)

- (instancetype)initWithManagedItem:(SecCDKeychainManagedItem*)managedItem keychain:(SecCDKeychain*)keychain error:(NSError**)error;

@end

@interface TestItemType : SecCDKeychainItemType
@end

@implementation TestItemType

+ (instancetype)itemType
{
return [[self alloc] _initWithName:@"TestItem" version:1 primaryKeys:nil syncableKeys:nil];
}

@end

@interface CDKeychainTests : KeychainXCTest
@end

@implementation CDKeychainTests {
SecCDKeychain* _keychain;
SFKeychainServerFakeConnection* _connection;
}

- (void)setUp
{
[super setUp];

NSURL* persistentStoreURL = (__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)@"CDKeychain");
NSBundle* resourcesBundle = [NSBundle bundleWithPath:@"/System/Library/Keychain/KeychainResources.bundle"];
NSURL* managedObjectModelURL = [resourcesBundle URLForResource:@"KeychainModel" withExtension:@"momd"];
_keychain = [[SecCDKeychain alloc] initWithStorageURL:persistentStoreURL modelURL:managedObjectModelURL encryptDatabase:false];
_connection = [[SFKeychainServerFakeConnection alloc] init];

self.keychainPartialMock = OCMPartialMock(_keychain);
[[[[self.keychainPartialMock stub] andCall:@selector(getDatabaseKeyDataithError:) onObject:self] ignoringNonObjectArgs] _onQueueGetDatabaseKeyDataWithError:NULL];

[_keychain _registerItemTypeForTesting:[TestItemType itemType]];
}

- (void)tearDown
{
[self.keychainPartialMock stopMocking];
self.keychainPartialMock = nil;

[super tearDown];
}

- (NSArray<SecCDKeychainLookupTuple*>*)lookupTuplesForAttributes:(NSDictionary*)attributes
{
NSMutableArray* lookupTuples = [[NSMutableArray alloc] initWithCapacity:attributes.count];
[attributes enumerateKeysAndObjectsUsingBlock:^(NSString* key, id value, BOOL* stop) {
[lookupTuples addObject:[SecCDKeychainLookupTuple lookupTupleWithKey:key value:value]];
}];
return lookupTuples;
}

- (void)testInsertAndRetrieveItemWithPersistentID
{
SecCDKeychainAccessControlEntity* owner = [SecCDKeychainAccessControlEntity accessControlEntityWithType:SecCDKeychainAccessControlEntityTypeAccessGroup stringRepresentation:@"com.apple.token"];

NSUUID* persistentID = [NSUUID UUID];
SecCDKeychainItem* item = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:persistentID attributes:@{@"key" : @"value"} lookupAttributes:nil secrets:@{@"data" : @"MySecret"} owner:owner keyclass:key_class_ak];

XCTestExpectation* insertExpectation = [self expectationWithDescription:@"insert item"];
[_keychain insertItems:@[item] withConnection:_connection completionHandler:^(bool success, NSError* error) {
XCTAssertTrue(success, @"should have been able to insert item");
XCTAssertNil(error, @"should not have gotten an error inserting item");
[insertExpectation fulfill];
}];
[self waitForExpectations:@[insertExpectation] timeout:5.0];

XCTestExpectation* fetchExpectation = [self expectationWithDescription:@"fetch item"];
[_keychain fetchItemForPersistentID:persistentID withConnection:_connection completionHandler:^(SecCDKeychainItem* fetchedItem, NSError* error) {
XCTAssertNotNil(fetchedItem, @"should have been able to fetch item");
XCTAssertNil(error, @"should not have gotten an error fetching item");
XCTAssertEqualObjects(fetchedItem, item, @"fetched item should match inserted item");
[fetchExpectation fulfill];
}];
[self waitForExpectations:@[fetchExpectation] timeout:5.0];
}

- (SecCDKeychainItem*)fullItemForItemMetadata:(SecCDKeychainItemMetadata*)metadata
{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block SecCDKeychainItem* result = nil;
[metadata fetchFullItemWithKeychain:_keychain withConnection:_connection completionHandler:^(SecCDKeychainItem* item, NSError* error) {
result = item;
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
return result;
}

- (void)testLookupItemByAttribute
{
NSUUID* firstItemID = [NSUUID UUID];
NSUUID* secondItemID = [NSUUID UUID];
NSDictionary* firstItemAttributes = @{@"attribute1" : @"gotIt", @"sharedAttribute" : @"first"};
NSDictionary* secondItemAttributes = @{@"attribute2" : @"gotIt", @"sharedAttribute" : @"second"};
NSArray* firstItemLookupAttributes = [self lookupTuplesForAttributes:firstItemAttributes];
NSArray* secondItemLookupAttributes = [self lookupTuplesForAttributes:secondItemAttributes];

SecCDKeychainAccessControlEntity* owner = [SecCDKeychainAccessControlEntity accessControlEntityWithType:SecCDKeychainAccessControlEntityTypeAccessGroup stringRepresentation:@"com.apple.token"];
SecCDKeychainItem* firstItem = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:firstItemID attributes:firstItemAttributes lookupAttributes:firstItemLookupAttributes secrets:@{@"data" : @"I'm the first"} owner:owner keyclass:key_class_ak];
SecCDKeychainItem* secondItem = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:secondItemID attributes:secondItemAttributes lookupAttributes:secondItemLookupAttributes secrets:@{@"data" : @"I'm the second"} owner:owner keyclass:key_class_ak];

XCTestExpectation* insertExpection = [self expectationWithDescription:@"insert items"];
[_keychain insertItems:@[firstItem, secondItem] withConnection:_connection completionHandler:^(bool success, NSError* error) {
XCTAssertTrue(success, @"failed to successfully insert items with error: %@", error);
XCTAssertNil(error, @"encountered error inserting items: %@", error);
[insertExpection fulfill];
}];
[self waitForExpectations:@[insertExpection] timeout:5.0];

XCTestExpectation* fetchExpectation = [self expectationWithDescription:@"fetch items"];
[_keychain fetchItemsWithValue:@"gotIt" forLookupKey:@"attribute1" ofType:SecCDKeychainLookupValueTypeString withConnection:_connection completionHandler:^(NSArray<SecCDKeychainItemMetadata*>* items, NSError* error) {
XCTAssertEqual(items.count, (unsigned long)1, @"did not get the expected number of fetched items; expected 1, but got %d", (int)items.count);
SecCDKeychainItem* fetchedItem = [self fullItemForItemMetadata:items.firstObject];
XCTAssertNotNil(fetchedItem, @"failed to fetch item we just inserted with error: %@", error);
XCTAssertNil(error, @"encountered error fetching item: %@", error);
XCTAssertEqualObjects(fetchedItem, firstItem, @"fetched item does not match the item we expected to fetch");
[fetchExpectation fulfill];
}];

XCTestExpectation* secondFetchExpectation = [self expectationWithDescription:@"second fetch"];
[_keychain fetchItemsWithValue:@"first" forLookupKey:@"sharedAttribute" ofType:SecCDKeychainLookupValueTypeString withConnection:_connection completionHandler:^(NSArray<SecCDKeychainItemMetadata*>* items, NSError* error) {
XCTAssertEqual(items.count, (unsigned long)1, @"did not get the expected number of fetched items; expected 1, but got %d", (int)items.count);
SecCDKeychainItem* fetchedItem = [self fullItemForItemMetadata:items.firstObject];
XCTAssertNotNil(fetchedItem, @"failed to fetch item we just inserted with error: %@", error);
XCTAssertNil(error, @"encountered error fetching item: %@", error);
XCTAssertEqualObjects(fetchedItem, firstItem, @"fetched item does not match the item we expected to fetch");
[secondFetchExpectation fulfill];
}];
[self waitForExpectations:@[fetchExpectation, secondFetchExpectation] timeout:5.0];
}

- (void)testLookupMultipleItems
{
NSUUID* firstItemID = [NSUUID UUID];
NSUUID* secondItemID = [NSUUID UUID];
NSUUID* thirdUUID = [NSUUID UUID];
NSDictionary* firstItemAttributes = @{@"attribute1" : @"gotIt", @"sharedAttribute" : @"bothHaveThis"};
NSDictionary* secondItemAttributes = @{@"attribute2" : @"gotIt", @"sharedAttribute" : @"bothHaveThis"};
NSDictionary* thirdItemAttributes = @{@"attribute3" : @"gotIt", @"sharedAttribute" : @"somethingDifferent"};
NSArray* firstItemLookupAttributes = [self lookupTuplesForAttributes:firstItemAttributes];
NSArray* secondItemLookupAttributes = [self lookupTuplesForAttributes:secondItemAttributes];
NSArray* thirdItemLookupAttributes = [self lookupTuplesForAttributes:thirdItemAttributes];

SecCDKeychainAccessControlEntity* owner = [SecCDKeychainAccessControlEntity accessControlEntityWithType:SecCDKeychainAccessControlEntityTypeAccessGroup stringRepresentation:@"com.apple.token"];
SecCDKeychainItem* firstItem = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:firstItemID attributes:firstItemAttributes lookupAttributes:firstItemLookupAttributes secrets:@{@"data" : @"I'm the first"} owner:owner keyclass:key_class_ak];
SecCDKeychainItem* secondItem = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:secondItemID attributes:secondItemAttributes lookupAttributes:secondItemLookupAttributes secrets:@{@"data" : @"I'm the second"} owner:owner keyclass:key_class_ak];
SecCDKeychainItem* thirdItem = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:thirdUUID attributes:thirdItemAttributes lookupAttributes:thirdItemLookupAttributes secrets:@{@"data" : @"I'm the third"} owner:owner keyclass:key_class_ak];

XCTestExpectation* insertExpectation = [self expectationWithDescription:@"insert items"];
[_keychain insertItems:@[firstItem, secondItem] withConnection:_connection completionHandler:^(bool success, NSError* error) {
XCTAssertTrue(success, @"failed to successfully insert items with error: %@", error);
XCTAssertNil(error, @"encountered error inserting items: %@", error);
[insertExpectation fulfill];
}];
[self waitForExpectations:@[insertExpectation] timeout:5.0];

XCTestExpectation* fetchExpectation = [self expectationWithDescription:@"fetch items"];
[_keychain fetchItemsWithValue:@"bothHaveThis" forLookupKey:@"sharedAttribute" ofType:SecCDKeychainLookupValueTypeString withConnection:_connection completionHandler:^(NSArray<SecCDKeychainItemMetadata*>* items, NSError* error) {
XCTAssertEqual(items.count, (unsigned long)2, @"did not get the expected number of fetched items; expected 2, but got %d", (int)items.count);
XCTAssertNil(error, @"encountered error fetching item: %@", error);
if (items.count >= 2) {
SecCDKeychainItem* firstFetchedObject = [self fullItemForItemMetadata:items[0]];
SecCDKeychainItem* secondFetchedObject = [self fullItemForItemMetadata:items[1]];
XCTAssertTrue([firstFetchedObject isEqual:firstItem] || [firstFetchedObject isEqual:secondItem], @"first fetched object does not match either of the items we expected to fetch");
XCTAssertTrue([secondFetchedObject isEqual:firstItem] || [secondFetchedObject isEqual:secondItem], @"second fetched object does not match either of the items we expected to fetch");
XCTAssertFalse([firstFetchedObject isEqual:secondFetchedObject], @"the objects we got back from our query are unexpectedly equal to each other");
XCTAssertFalse([items containsObject:thirdItem.metadata], @"found an unexpected item in our fetch results");
}
[fetchExpectation fulfill];
}];
[self waitForExpectations:@[fetchExpectation] timeout:5.0];
}

- (void)testDuplicates
{
// TODO: test some duplicate-rejection scenarios, including the case where no primary keys are explicitly defined
}

- (void)testAccessGroups
{
[_connection setFakeAccessGroups:[NSArray arrayWithObject:@"TestAccessGroup"]];

// first we'll try inserting in an access group that should be rejected
SecCDKeychainAccessControlEntity* badOwner = [SecCDKeychainAccessControlEntity accessControlEntityWithType:SecCDKeychainAccessControlEntityTypeAccessGroup stringRepresentation:@"BadAccessGroup"];
NSUUID* persistentID = [NSUUID UUID];
SecCDKeychainItem* badItem = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:persistentID attributes:@{@"key" : @"value"} lookupAttributes:nil secrets:@{@"data" : @"MySecret"} owner:badOwner keyclass:key_class_ak];

XCTestExpectation* badAccessGroupInsertExpectation = [self expectationWithDescription:@"items insert with bad access group"];
[_keychain insertItems:@[badItem] withConnection:_connection completionHandler:^(bool success, NSError* error) {
XCTAssertFalse(success, @"should have rejected insert of item for a bad access group");
XCTAssertNotNil(error, @"should have encountered an error for inserting with bad access group");
XCTAssertEqualObjects(error.domain, SFKeychainErrorDomain, @"should have gotten an error in the SFKeychainErrorDomain");
XCTAssertEqual(error.code, SFKeychainErrorInvalidAccessGroup, @"should have gotten the SFKeychainErrorInvalidAccessGroup error code");
[badAccessGroupInsertExpectation fulfill];
}];
[self waitForExpectations:@[badAccessGroupInsertExpectation] timeout:5.0];

// ok, now try an insertion that should succeed
SecCDKeychainAccessControlEntity* goodOwner = [SecCDKeychainAccessControlEntity accessControlEntityWithType:SecCDKeychainAccessControlEntityTypeAccessGroup stringRepresentation:@"TestAccessGroup"];
SecCDKeychainItem* item = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:persistentID attributes:@{@"key" : @"value"} lookupAttributes:nil secrets:@{@"data" : @"MySecret"} owner:goodOwner keyclass:key_class_ak];

XCTestExpectation* insertExpectation = [self expectationWithDescription:@"items inserted"];
[_keychain insertItems:@[item] withConnection:_connection completionHandler:^(bool success, NSError* error) {
XCTAssertTrue(success, @"should have been able to insert item");
XCTAssertNil(error, @"should not have gotten an error inserting item");
[insertExpectation fulfill];
}];
[self waitForExpectations:@[insertExpectation] timeout:5.0];

XCTestExpectation* fetchExpectation = [self expectationWithDescription:@"fetch item"];
[_keychain fetchItemForPersistentID:persistentID withConnection:_connection completionHandler:^(SecCDKeychainItem* fetchedItem, NSError* error) {
XCTAssertNotNil(fetchedItem, @"should have been able to fetch item");
XCTAssertNil(error, @"should not have gotten error fetching item");
XCTAssertEqualObjects(fetchedItem, item, @"fetched item should match the inserted item");
[fetchExpectation fulfill];
}];
[self waitForExpectations:@[fetchExpectation] timeout:5.0];

// now change my access group and see if I get the expected error trying to fetch the item
[_connection setFakeAccessGroups:[NSArray arrayWithObject:@"NotYourAccessGroup"]];

XCTestExpectation* badFetchExpectation = [self expectationWithDescription:@"fetch item with bad access group"];
[_keychain fetchItemForPersistentID:persistentID withConnection:_connection completionHandler:^(SecCDKeychainItem* fetchedItem, NSError* error) {
XCTAssertNil(fetchedItem, @"should not have gotten item when fetching with a bad access group");
XCTAssertNotNil(error, @"should have gotten an error fetching item");
XCTAssertEqualObjects(error.domain, SFKeychainErrorDomain, @"should have gotten an error in the SFKeychainErrorDomain");
XCTAssertEqual(error.code, SFKeychainErrorItemNotFound, @"should have gotten the SFKeychainErrorItemNotFound error code");
[badFetchExpectation fulfill];
}];
[self waitForExpectations:@[badFetchExpectation] timeout:5.0];

// now try to delete the thing to confirm we cannot

XCTestExpectation* badDeleteExpectation = [self expectationWithDescription:@"delete item with bad access group"];
[_keychain deleteItemWithPersistentID:persistentID withConnection:_connection completionHandler:^(bool success, NSError* error) {
XCTAssertFalse(success, @"should not have succeeded at deleting item we don't have access to");
XCTAssertNotNil(error, @"should have gotten an error deleting item we don't have access to");
XCTAssertEqualObjects(error.domain, SFKeychainErrorDomain, @"should have gotten an error in the SFKeychainErrorDomain");
XCTAssertEqual(error.code, SFKeychainErrorItemNotFound, @"should have gotten the SFKeychainErrorItemNotFound error code");
[badDeleteExpectation fulfill];
}];
[self waitForExpectations:@[badDeleteExpectation] timeout:5.0];

// switch to the good access group to make sure it's still there
[_connection setFakeAccessGroups:[NSArray arrayWithObject:@"TestAccessGroup"]];

XCTestExpectation* fetchAgainExpecation = [self expectationWithDescription:@"fetch item again"];
[_keychain fetchItemForPersistentID:persistentID withConnection:_connection completionHandler:^(SecCDKeychainItem* fetchedItem, NSError* error) {
XCTAssertNotNil(fetchedItem, @"should have been able to fetch item");
XCTAssertNil(error, @"should not have gotten error fetching item");
XCTAssertEqualObjects(fetchedItem, item, @"fetched item should match the inserted item");
[fetchAgainExpecation fulfill];
}];
[self waitForExpectations:@[fetchAgainExpecation] timeout:5.0];

// and finally delete with proper permissions

XCTestExpectation* deleteExpectation = [self expectationWithDescription:@"delete item"];
[_keychain deleteItemWithPersistentID:persistentID withConnection:_connection completionHandler:^(bool success, NSError* error) {
XCTAssertTrue(success, @"should have succeeded deleting item");
XCTAssertNil(error, @"should not have gotten error deleting item: %@", error);
[deleteExpectation fulfill];
}];
[self waitForExpectations:@[deleteExpectation] timeout:5.0];

XCTestExpectation* postDeleteFetchExpectation = [self expectationWithDescription:@"fetch item after delete"];
[_keychain fetchItemForPersistentID:persistentID withConnection:_connection completionHandler:^(SecCDKeychainItem* fetchedItem, NSError* error) {
XCTAssertNil(fetchedItem, @"should not have gotten item when fetching with a bad access group");
XCTAssertNotNil(error, @"should have gotten error for fetch of deleted item");
XCTAssertEqualObjects(error.domain, SFKeychainErrorDomain, @"should have gotten an error in the SFKeychainErrorDomain");
XCTAssertEqual(error.code, SFKeychainErrorItemNotFound, @"should have gotten the SFKeychainErrorItemNotFound error code");
[postDeleteFetchExpectation fulfill];
}];
[self waitForExpectations:@[postDeleteFetchExpectation] timeout:5.0];
}

- (void)testDifferentOwnersWithSimilarItems
{
[_connection setFakeAccessGroups:[NSArray arrayWithObject:@"FirstAccessGroup"]];
SecCDKeychainAccessControlEntity* firstOwner = [SecCDKeychainAccessControlEntity accessControlEntityWithType:SecCDKeychainAccessControlEntityTypeAccessGroup stringRepresentation:@"FirstAccessGroup"];

NSUUID* firstPersistentID = [NSUUID UUID];
SecCDKeychainItem* firstItem = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:firstPersistentID attributes:@{@"key" : @"value"} lookupAttributes:nil secrets:@{@"data" : @"MyFirstSecret"} owner:firstOwner keyclass:key_class_ak];

XCTestExpectation* firstInsertExpectation = [self expectationWithDescription:@"insert first item"];
[_keychain insertItems:@[firstItem] withConnection:_connection completionHandler:^(bool success, NSError* error) {
XCTAssertTrue(success, @"should have been able to insert item");
XCTAssertNil(error, @"should not have gotten an error inserting item");
[firstInsertExpectation fulfill];
}];
[self waitForExpectations:@[firstInsertExpectation] timeout:5.0];

[_connection setFakeAccessGroups:[NSArray arrayWithObject:@"SecondAccessGroup"]];
SecCDKeychainAccessControlEntity* secondOwner = [SecCDKeychainAccessControlEntity accessControlEntityWithType:SecCDKeychainAccessControlEntityTypeAccessGroup stringRepresentation:@"SecondAccessGroup"];

NSUUID* secondPersistentID = [NSUUID UUID];
SecCDKeychainItem* secondItem = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:secondPersistentID attributes:@{@"key" : @"value"} lookupAttributes:nil secrets:@{@"data" : @"MySecondSecret"} owner:secondOwner keyclass:key_class_ak];

XCTestExpectation* secondInsertExpectation = [self expectationWithDescription:@"insert item"];
[_keychain insertItems:@[secondItem] withConnection:_connection completionHandler:^(bool success, NSError* error) {
XCTAssertTrue(success, @"should have been able to insert item");
XCTAssertNil(error, @"should not have gotten an error inserting item");
[secondInsertExpectation fulfill];
}];
[self waitForExpectations:@[secondInsertExpectation] timeout:5.0];

// now try fetching the first and second items
// we should have access to the second but not the first

XCTestExpectation* firstFetchExpectation = [self expectationWithDescription:@"fetch first item"];
[_keychain fetchItemForPersistentID:firstPersistentID withConnection:_connection completionHandler:^(SecCDKeychainItem* fetchedItem, NSError* error) {
XCTAssertNil(fetchedItem, @"should not have been able to fetch item from first access group");
XCTAssertNotNil(error, @"should have gotten an error fetching item");
XCTAssertEqualObjects(error.domain, SFKeychainErrorDomain, @"should have gotten an error in the SFKeychainErrorDomain");
XCTAssertEqual(error.code, SFKeychainErrorItemNotFound, @"should have gotten the SFKeychainErrorItemNotFound error code");
[firstFetchExpectation fulfill];
}];
[self waitForExpectations:@[firstFetchExpectation] timeout:5.0];

XCTestExpectation* secondFetchExpectation = [self expectationWithDescription:@"fetch second item"];
[_keychain fetchItemForPersistentID:secondPersistentID withConnection:_connection completionHandler:^(SecCDKeychainItem* fetchedItem, NSError* error) {
XCTAssertNotNil(fetchedItem, @"should have been able to fetch item");
XCTAssertNil(error, @"should not have gotten an error fetching item");
XCTAssertEqualObjects(fetchedItem, secondItem, @"fetched item should match inserted item");
[secondFetchExpectation fulfill];
}];
[self waitForExpectations:@[secondFetchExpectation] timeout:5.0];

// finally, do a search; make sure it returns exactly one result
XCTestExpectation* searchExpectation = [self expectationWithDescription:@"search items"];
[_keychain fetchItemsWithValue:@"value" forLookupKey:@"key" ofType:SecCDKeychainLookupValueTypeString withConnection:_connection completionHandler:^(NSArray<SecCDKeychainItemMetadata*>* items, NSError* error) {
XCTAssertEqual(items.count, (unsigned long)1, @"should have gotten 1 search result, but got %d", (int)items.count);
SecCDKeychainItem* fetchedItem = [self fullItemForItemMetadata:items.firstObject];
XCTAssertNotNil(fetchedItem, @"should have been able to fetch full item");
XCTAssertNil(error, @"should not have gotten error looking up item");
XCTAssertEqualObjects(fetchedItem, secondItem, @"item we looked up should match the one we inserted");
[searchExpectation fulfill];
}];
[self waitForExpectations:@[searchExpectation] timeout:5.0];
}

- (void)testTamperedMetadata
{
SecCDKeychainAccessControlEntity* owner = [SecCDKeychainAccessControlEntity accessControlEntityWithType:SecCDKeychainAccessControlEntityTypeAccessGroup stringRepresentation:@"com.apple.token"];

NSUUID* persistentID = [NSUUID UUID];
SecCDKeychainItem* item = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:persistentID attributes:@{@"key" : @"value"} lookupAttributes:nil secrets:@{@"data" : @"MyFirstSecret"} owner:owner keyclass:key_class_ak];

XCTestExpectation* insertExpectation = [self expectationWithDescription:@"insert item"];
[_keychain insertItems:@[item] withConnection:_connection completionHandler:^(bool success, NSError* error) {
XCTAssertTrue(success, @"should have been able to insert item");
XCTAssertNil(error, @"should not have gotten an error inserting item");
[insertExpectation fulfill];
}];
[self waitForExpectations:@[insertExpectation] timeout:5.0];

XCTestExpectation* tamperExpectation = [self expectationWithDescription:@"tamper with item"];
[_keychain performOnManagedObjectQueue:^(NSManagedObjectContext* managedObjectContext, NSError* managedObjectError) {
XCTAssertNotNil(managedObjectContext, @"should have gotten a managed object to perform work with");
XCTAssertNil(managedObjectError, @"should not get error performing a block on the managed object context");

NSError* error = nil;
SecCDKeychainManagedItem* managedItem = [self->_keychain fetchManagedItemForPersistentID:persistentID withManagedObjectContext:managedObjectContext error:&error];
XCTAssertNotNil(managedItem, @"should have been able to fetch the managed item we just inserted");
XCTAssertNil(error, @"should not have gotten an error fetching the managed item");

// ok, now let's do something nefarious, like change the metadata
NSDictionary* fakeMetadata = @{@"key" : @"differentValue"};
managedItem.metadata = [NSPropertyListSerialization dataWithPropertyList:fakeMetadata format:NSPropertyListXMLFormat_v1_0 options:9 error:&error];
XCTAssertNotNil(managedItem.metadata, @"should have been able to write modified metadata");
XCTAssertNil(error, @"should not have gotten an error writing modified metadata");

SecCDKeychainItem* brokenItem = [[SecCDKeychainItem alloc] initWithManagedItem:managedItem keychain:self->_keychain error:&error];
XCTAssertNil(brokenItem, @"should not have been able to create item with tampered metadata");
XCTAssertNotNil(error, @"should have gotten an error attempting to create an item with tampered metadata");
XCTAssertEqualObjects(error.domain, SFKeychainErrorDomain, @"should have gotten an error in the SFKeychainErrorDomain");
XCTAssertEqual(error.code, SFKeychainErrorItemDecryptionFailed, @"should have gotten the SFKeychainErrorItemDecryptionFailed error");

[tamperExpectation fulfill];
}];
[self waitForExpectations:@[tamperExpectation] timeout:5.0];
}

@end

// these tests do not wish to have a keychain already setup
@interface CDKeychainSetupTests : KeychainXCTest
@end

@implementation CDKeychainSetupTests {
SFKeychainServerFakeConnection* _connection;
}

- (void)setUp
{
[super setUp];

_connection = [[SFKeychainServerFakeConnection alloc] init];
}

- (void)testKeychainLocking
{
self.lockState = LockStateLockedAndDisallowAKS;

NSURL* persistentStoreURL = (__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)@"CDKeychain");
NSBundle* resourcesBundle = [NSBundle bundleWithPath:@"/System/Library/Keychain/KeychainResources.bundle"];
NSURL* managedObjectModelURL = [resourcesBundle URLForResource:@"KeychainModel" withExtension:@"momd"];
SecCDKeychain* keychain = [[SecCDKeychain alloc] initWithStorageURL:persistentStoreURL modelURL:managedObjectModelURL encryptDatabase:false];
XCTAssertNotNil(keychain, @"should have been able to create a keychain instance");

self.keychainPartialMock = OCMPartialMock(keychain);
[[[[self.keychainPartialMock stub] andCall:@selector(getDatabaseKeyDataithError:) onObject:self] ignoringNonObjectArgs] _onQueueGetDatabaseKeyDataWithError:NULL];

SecCDKeychainAccessControlEntity* owner = [SecCDKeychainAccessControlEntity accessControlEntityWithType:SecCDKeychainAccessControlEntityTypeAccessGroup stringRepresentation:@"com.apple.token"];

NSUUID* persistentID = [NSUUID UUID];
SecCDKeychainItem* item = [[SecCDKeychainItem alloc] initItemType:[TestItemType itemType] withPersistentID:persistentID attributes:@{@"key" : @"value"} lookupAttributes:nil secrets:@{@"data" : @"MySecret"} owner:owner keyclass:key_class_ak];

XCTestExpectation* insertExpectation = [self expectationWithDescription:@"insert item"];
[keychain insertItems:@[item] withConnection:_connection completionHandler:^(bool success, NSError* error) {
XCTAssertFalse(success, @"should not be able to insert item while locked");
XCTAssertNotNil(error, @"should get error inserting item while locked");

// in the future, check for proper error
// // <rdar://problem/38972671> add SFKeychainErrorDeviceLocked

[insertExpectation fulfill];
}];
[self waitForExpectations:@[insertExpectation] timeout:5.0];

self.lockState = LockStateUnlocked;

[keychain _registerItemTypeForTesting:[TestItemType itemType]];

XCTestExpectation* insertAgainExpectation = [self expectationWithDescription:@"insert item again"];
[keychain insertItems:@[item] withConnection:_connection completionHandler:^(bool success, NSError* error) {
XCTAssertTrue(success, @"should be able to insert item after unlock");
XCTAssertNil(error, @"should not get error inserting after unlock");
[insertAgainExpectation fulfill];
}];
[self waitForExpectations:@[insertAgainExpectation] timeout:5.0];

XCTestExpectation* fetchExpectation = [self expectationWithDescription:@"fetch item"];
[keychain fetchItemForPersistentID:persistentID withConnection:_connection completionHandler:^(SecCDKeychainItem* fetchedItem, NSError* error) {
XCTAssertNotNil(fetchedItem, @"should have been able to fetch item");
XCTAssertNil(error, @"should not have gotten an error fetching item");
XCTAssertEqualObjects(fetchedItem, item, @"fetched item should match inserted item");
[fetchExpectation fulfill];
}];
[self waitForExpectations:@[fetchExpectation] timeout:5.0];

self.lockState = LockStateLockedAndDisallowAKS;
dispatch_sync(keychain.queue, ^{
[keychain _onQueueDropClassAPersistentStore];
});

XCTestExpectation* fetchWhileLockedExpectation = [self expectationWithDescription:@"fetch item while locked"];
[keychain fetchItemForPersistentID:persistentID withConnection:_connection completionHandler:^(SecCDKeychainItem* fetchedItem, NSError* error) {
XCTAssertNil(fetchedItem, @"should not be able to fetch item while locked");
XCTAssertNotNil(error, @"should get an error fetching item while locked");

// in the future, check for proper error
// // <rdar://problem/38972671> add SFKeychainErrorDeviceLocked

[fetchWhileLockedExpectation fulfill];
}];
[self waitForExpectations:@[fetchWhileLockedExpectation] timeout:5.0];
}

@end

#endif