/*
 * 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 <TargetConditionals.h>

#if !TARGET_OS_BRIDGE

#import "SecCDKeychain.h"
#import "SecCDKeychainManagedItem+CoreDataClass.h"
#import "SecCDKeychainManagedLookupEntry+CoreDataClass.h"
#import "SecCDKeychainManagedItemType+CoreDataClass.h"
#import "SecCDKeychainManagedAccessControlEntity+CoreDataClass.h"
#import "SecFileLocations.h"
#import "SecItemServer.h"
#import "SecItem.h"
#import "SecItemPriv.h"
#import <Security/SecBase.h>
#import "SFKeychainServer.h"
#import "CloudKitCategories.h"
#import "securityd_client.h"
#import "SecKeybagSupport.h"
#import "SecKeybagSupport.h"
#import "keychain/categories/NSError+UsefulConstructors.h"
#import <SecurityFoundation/SFKeychain.h>
#import <SecurityFoundation/SFDigestOperation.h>
#import <SecurityFoundation/SFKey.h>
#import <SecurityFoundation/SFEncryptionOperation.h>
#import <CoreData/NSPersistentStoreCoordinator_Private.h>
#if USE_KEYSTORE
#if __has_include(<libaks_ref_key.h>)
#import <libaks_ref_key.h>
#endif
#endif
#import <Foundation/NSData_Private.h>
#import <notify.h>

static NSString* const SecCDKeychainErrorDomain = @"com.apple.security.cdkeychain";

static NSString* const SecCDKeychainEntityLookupEntry = @"LookupEntry";
static NSString* const SecCDKeychainEntityItem = @"Item";
static NSString* const SecCDKeychainEntityItemType = @"ItemType";
static NSString* const SecCDKeychainEntityTypeAccessControlEntity = @"AccessControlEntity";

static NSString* const SecCDKeychainItemMetadataSHA256 = @"SecCDKeychainItemMetadataSHA256";

static const NSInteger SecCDKeychainErrorDeserializing = 1;
static const NSInteger SecCDKeychainErrorInternal = 2;
//static const NSInteger SecCDKeychainErrorMetadataDoesNotMatch = 3;

SecCDKeychainLookupValueType* const SecCDKeychainLookupValueTypeString = (SecCDKeychainLookupValueType*)@"SecCDKeychainLookupValueTypeString";
SecCDKeychainLookupValueType* const SecCDKeychainLookupValueTypeData = (SecCDKeychainLookupValueType*)@"SecCDKeychainLookupValueTypeData";
SecCDKeychainLookupValueType* const SecCDKeychainLookupValueTypeNumber = (SecCDKeychainLookupValueType*)@"SecCDKeychainLookupValueTypeNumber";
SecCDKeychainLookupValueType* const SecCDKeychainLookupValueTypeDate = (SecCDKeychainLookupValueType*)@"SecCDKeychainLookupValueTypeDate";

@interface SecCDKeychainItem ()

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

- (NSString*)primaryKeyStringRepresentationWithError:(NSError**)error;
- (NSData*)encryptedSecretDataWithAttributeData:(NSData*)attributeData keybag:(keybag_handle_t)keybag error:(NSError**)error;

@end

@interface SecCDKeychainItemMetadata ()

@property (readonly, copy) NSSet<SecCDKeychainLookupTuple*>* lookupAttributesSet;
@property (readonly, copy) NSData* managedDataBlob;

- (instancetype)initWithItemType:(SecCDKeychainItemType*)itemType persistentID:(NSUUID*)persistentID attributes:(NSDictionary*)attributes lookupAttributes:(NSArray<SecCDKeychainLookupTuple*>*)lookupAttributes owner:(SecCDKeychainAccessControlEntity*)owner keyclass:(keyclass_t)keyclass;
- (instancetype)initWithManagedItem:(SecCDKeychainManagedItem*)managedItem keychain:(SecCDKeychain*)keychain error:(NSError**)error;

@end

@interface SecCDKeychainLookupTuple ()

+ (instancetype)lookupTupleWithManagedLookupEntry:(SecCDKeychainManagedLookupEntry*)lookupEntry;

@end

@interface SecCDKeychainItemType ()

- (SecCDKeychainManagedItemType*)managedItemTypeWithContext:(NSManagedObjectContext*)managedObjectContext error:(NSError**)error;

@end

@interface SecCDKeychainAccessControlEntity ()

- (instancetype)initWithManagedEntity:(SecCDKeychainManagedAccessControlEntity*)managedAccessControlEntity;

@end

@interface SecCDKeychainItemWrappedSecretData : NSObject <NSSecureCoding>

@property (readonly) SFAuthenticatedCiphertext* ciphertext;
@property (readonly) NSData* wrappedKeyData;
@property (readonly) NSData* refKeyBlob;

- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithCiphertext:(SFAuthenticatedCiphertext*)ciphertext wrappedKeyData:(NSData*)wrappedKeyData refKeyBlob:(NSData*)refKeyBlob;

@end

@implementation SecCDKeychainItemWrappedSecretData {
    SFAuthenticatedCiphertext* _ciphertext;
    NSData* _wrappedKeyData;
    NSData* _refKeyBlob;
}

@synthesize ciphertext = _ciphertext;
@synthesize wrappedKeyData = _wrappedKeyData;
@synthesize refKeyBlob = _refKeyBlob;

+ (BOOL)supportsSecureCoding
{
    return YES;
}

- (instancetype)initWithCiphertext:(SFAuthenticatedCiphertext*)ciphertext wrappedKeyData:(NSData*)wrappedKeyData refKeyBlob:(NSData*)refKeyBlob
{
    if (self = [super init]) {
        _ciphertext = ciphertext;
        _wrappedKeyData = wrappedKeyData.copy;
        _refKeyBlob = refKeyBlob.copy;
    }

    return self;
}

- (instancetype)initWithCoder:(NSCoder*)coder
{
    if (self = [super init]) {
        _ciphertext = [coder decodeObjectOfClass:[SFAuthenticatedCiphertext class] forKey:@"SecCDKeychainItemCiphertext"];
        _wrappedKeyData = [coder decodeObjectOfClass:[NSData class] forKey:@"SecCDKeychainItemWrappedKey"];
        _refKeyBlob = [coder decodeObjectOfClass:[NSData class] forKey:@"SecCDKeychainItemRefKeyBlob"];

        if (!_ciphertext || !_wrappedKeyData || !_refKeyBlob) {
            self = nil;
            secerror("SecCDKeychain: failed to deserialize wrapped secret data");
            [coder failWithError:[NSError errorWithDomain:SecCDKeychainErrorDomain code:SecCDKeychainErrorDeserializing userInfo:@{NSLocalizedDescriptionKey : @"failed to deserialize wrapped secret data"}]];
        }
    }

    return self;
}

- (void)encodeWithCoder:(NSCoder*)coder
{
    [coder encodeObject:_ciphertext forKey:@"SecCDKeychainItemCiphertext"];
    [coder encodeObject:_wrappedKeyData forKey:@"SecCDKeychainItemWrappedKey"];
    [coder encodeObject:_refKeyBlob forKey:@"SecCDKeychainItemRefKeyBlob"];
}

@end

#if USE_KEYSTORE

@implementation SecAKSRefKey {
    aks_ref_key_t _refKey;
}

- (instancetype)initWithKeybag:(keyclass_t)keybag keyclass:(keyclass_t)keyclass
{
    if (self = [super init]) {
        if (aks_ref_key_create(keybag, keyclass, key_type_sym, NULL, 0, &_refKey) != kAKSReturnSuccess) {
            self = nil;
        }
    }

    return self;
}

- (instancetype)initWithBlob:(NSData*)blob keybag:(keybag_handle_t)keybag
{
    if (self = [super init]) {
        if (aks_ref_key_create_with_blob(keybag, blob.bytes, blob.length, &_refKey) != kAKSReturnSuccess) {
            self = nil;
        }
    }

    return self;
}

- (void)dealloc
{
    aks_ref_key_free(&_refKey);
}

- (NSData*)wrappedDataForKey:(SFAESKey*)key
{
    void* wrappedKeyBytes = NULL;
    size_t wrappedKeyLength = 0;
    if (aks_ref_key_wrap(_refKey, NULL, 0, key.keyData.bytes, key.keyData.length, &wrappedKeyBytes, &wrappedKeyLength) == kAKSReturnSuccess) {
        return [NSData dataWithBytesNoCopy:wrappedKeyBytes length:wrappedKeyLength];
    }

    return nil;
}

- (SFAESKey*)keyWithWrappedData:(NSData*)wrappedKeyData
{
    void* unwrappedKeyBytes = NULL;
    size_t unwrappedKeyLength = 0;
    int aksResult = aks_ref_key_unwrap(_refKey, NULL, 0, wrappedKeyData.bytes, wrappedKeyData.length, &unwrappedKeyBytes, &unwrappedKeyLength);
    if (aksResult != kAKSReturnSuccess || !unwrappedKeyBytes) {
        return nil;
    }

    NSData* keyData = [NSData dataWithBytesNoCopy:unwrappedKeyBytes length:unwrappedKeyLength];
    NSError* error = nil;
    SFAESKey* key = [[SFAESKey alloc] initWithData:keyData specifier:[[SFAESKeySpecifier alloc] initWithBitSize:SFAESKeyBitSize256] error:&error];
    if (!key) {
        secerror("SecCDKeychain: error creating AES key from unwrapped item key data with error: %@", error);
    }

    return key;
}

- (NSData*)refKeyBlob
{
    size_t refKeyBlobLength = 0;
    const uint8_t* refKeyBlobBytes = aks_ref_key_get_blob(_refKey, &refKeyBlobLength);
    return [NSData dataWithBytes:refKeyBlobBytes length:refKeyBlobLength];
}

@end

#endif // USE_KEYSTORE

@implementation SecCDKeychain {
    NSURL* _persistentStoreBaseURL;
    NSPersistentStoreCoordinator* _persistentStoreCoordinator;
    NSManagedObjectContext* _managedObjectContext;
    NSMutableDictionary* _managedItemTypeDict;
    NSMutableDictionary* _itemTypeDict;
    bool _encryptDatabase;
    dispatch_queue_t _queue;
    NSArray* _classAPersistentStores;
}

+ (SecCDKeychainLookupValueType*)lookupValueTypeForObject:(id)object
{
    if ([object isKindOfClass:[NSString class]]) {
        return SecCDKeychainLookupValueTypeString;
    }
    else if ([object isKindOfClass:[NSData class]]) {
        return SecCDKeychainLookupValueTypeData;
    }
    else if ([object isKindOfClass:[NSDate class]]) {
        return SecCDKeychainLookupValueTypeDate;
    }
    else if ([object isKindOfClass:[NSNumber class]]) {
        return SecCDKeychainLookupValueTypeNumber;
    }
    else {
        return nil;
    }
}

- (instancetype)initWithStorageURL:(NSURL*)persistentStoreURL modelURL:(NSURL*)managedObjectURL encryptDatabase:(bool)encryptDatabase
{
    if (!persistentStoreURL) {
        secerror("SecCDKeychain: no persistent store URL, so we can't create or open a database");
        return nil;
    }
    if (!managedObjectURL) {
        secerror("SecCDKeychain: no managed object model URL, so we can't create or open a database");
        return nil;
    }
    
    if (self = [super init]) {
        _queue = dispatch_queue_create("SecCDKeychain", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);

        _persistentStoreBaseURL = persistentStoreURL.copy;
        _encryptDatabase = encryptDatabase;

        NSManagedObjectModel* managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:managedObjectURL];
        _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel];
    }

    return self;
}

- (NSData*)_onQueueGetDatabaseKeyDataWithError:(NSError**)error
{
    NSData* keyData = nil;
    NSDictionary* databaseKeyQuery = @{ (id)kSecClass : (id)kSecClassGenericPassword,
                                        (id)kSecAttrAccessGroup : @"com.apple.security.securityd",
                                        (id)kSecAttrAccessible : (id)kSecAttrAccessibleWhenUnlocked,
                                        (id)kSecUseDataProtectionKeychain : @(YES),
                                        (id)kSecAttrService : @"com.apple.security.keychain.ak",
                                        (id)kSecReturnData : @(YES) };

    CFTypeRef result = NULL;
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)databaseKeyQuery, &result);

    if (status == errSecItemNotFound) {
        NSMutableDictionary* addKeyQuery = databaseKeyQuery.mutableCopy;
        [addKeyQuery removeObjectForKey:(id)kSecReturnData];

        uint8_t* keyBytes = malloc(16);
        if (SecRandomCopyBytes(NULL, 16, keyBytes) != 0) {
            secerror("SecCDKeychain: failed to create random key for CD database encryption - this means we won't be able to create a database");
            if (error) {
                *error = [NSError errorWithDomain:SecCDKeychainErrorDomain code:SecCDKeychainErrorInternal userInfo:@{NSLocalizedDescriptionKey : @"failed to create random key for CD database encryption"}];
            }
            return nil;
        }

        keyData = [NSData _newZeroingDataWithBytesNoCopy:keyBytes length:16 deallocator:nil];
        addKeyQuery[(id)kSecValueData] = keyData;
        status = SecItemAdd((__bridge CFDictionaryRef)addKeyQuery, NULL);
        if (status == errSecSuccess) {
            return keyData;
        }
        else {
            secerror("SecCDKeychain: failed to save encryption key to keychain, so bailing on database creation; status: %d", (int)status);
            CFErrorRef cfError = NULL;
            SecError(status, &cfError, CFSTR("failed to save encryption key to keychain, so bailing on database creation"));
            if (error) {
                *error = CFBridgingRelease(cfError);
                cfError = NULL;
            }
            else {
                CFReleaseNull(cfError);
            }
            return nil;
        }
    }
    else if (status == errSecInteractionNotAllowed) {
        //// <rdar://problem/38972671> add SFKeychainErrorDeviceLocked

        secerror("SecCDKeychain: can't create a class A store right now because the keychain is locked");
        CFErrorRef cfError = NULL;
        SecError(status, &cfError, CFSTR("can't create a class A store right now because the keychain is locked"));
        if (error) {
            *error = CFBridgingRelease(cfError);
            cfError = NULL;
        }
        else {
            CFReleaseNull(cfError);
        }
        return nil;
    }
    else if (status == errSecSuccess) {
        if ([(__bridge id)result isKindOfClass:[NSData class]]) {
            return (__bridge_transfer NSData*)result;
        }
        else {
            if (error) {
                *error = [NSError errorWithDomain:SFKeychainErrorDomain code:SFKeychainErrorInternal userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"result of keychain query for database key is wrong kind of class: %@", [(__bridge id)result class]]}];
            }
            
            CFReleaseNull(result);
            return nil;
        }
    }
    else {
        secerror("failed to save or retrieve key for CD database encryption - this means we won't be able to create a database; status: %d", (int)status);
        if (error) {
            *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:@{NSLocalizedDescriptionKey : @"failed to save or retrieve key for CD database encryption"}];
        }
        
        CFReleaseNull(result);
        return nil;
    }
}

- (NSManagedObjectContext*)_onQueueGetManagedObjectContextWithError:(NSError* __autoreleasing *)error
{
    dispatch_assert_queue(_queue);

    if (!_managedObjectContext) {
        NSURL* akStoreURL = [_persistentStoreBaseURL URLByAppendingPathExtension:@"ak"];

        NSData* keyData = [self _onQueueGetDatabaseKeyDataWithError:error];
        if (!keyData) {
            return nil;
        }

        if (_encryptDatabase) {
            NSPersistentStore* classAStore = [_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:akStoreURL options:@{NSSQLiteSEEKeychainItemOption : keyData} error:error];
            _classAPersistentStores = @[classAStore];
        }
        else {
            NSPersistentStore* classAStore = [_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:akStoreURL options:nil error:error];
            _classAPersistentStores = @[classAStore];
        }

        NSManagedObjectContext* managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        managedObjectContext.persistentStoreCoordinator = _persistentStoreCoordinator;

        // the first time around, setup our items types; grab old ones from the database and register any new ones
        // we can skip for subsequent loads of the store in the same run of securityd
        if (!_managedItemTypeDict || !_itemTypeDict) {
            _managedItemTypeDict = [NSMutableDictionary dictionary];
            _itemTypeDict = [NSMutableDictionary dictionary];
            [managedObjectContext performBlockAndWait:^{
                NSFetchRequest* managedItemTypeFetchRequest = [SecCDKeychainManagedItemType fetchRequest];
                NSArray<SecCDKeychainManagedItemType*>* managedItemTypes = [managedObjectContext executeFetchRequest:managedItemTypeFetchRequest error:error];
                if (managedItemTypes.count == 0) {
                    // TODO do some error handling here
                }
                else {
                    for (SecCDKeychainManagedItemType* managedItemType in managedItemTypes) {
                        NSMutableDictionary* itemTypeVersionDict = self->_managedItemTypeDict[managedItemType.name];
                        if (!itemTypeVersionDict) {
                            itemTypeVersionDict = [NSMutableDictionary dictionary];
                            self->_managedItemTypeDict[managedItemType.name] = itemTypeVersionDict;
                        }
                        itemTypeVersionDict[@(managedItemType.version)] = managedItemType;
                    }
                }

                [self registerItemType:[SecCDKeychainItemTypeCredential itemType] withManagedObjectContext:managedObjectContext];
            }];
        }

        _managedObjectContext = managedObjectContext;

        int token = 0;
        __weak __typeof(self) weakSelf = self;
        notify_register_dispatch(kUserKeybagStateChangeNotification, &token, _queue, ^(int t) {
            bool locked = true;
            CFErrorRef blockError = NULL;
            if (!SecAKSGetIsLocked(&locked, &blockError)) {
                secerror("SecDbKeychainMetadataKeyStore: error getting lock state: %@", blockError);
                CFReleaseNull(blockError);
            }

            if (locked) {
                [weakSelf _onQueueDropClassAPersistentStore];
            }
        });
    }

    return _managedObjectContext;
}

- (void)_onQueueDropClassAPersistentStore
{
    for (NSPersistentStore* store in _classAPersistentStores) {
        NSError* error = nil;
        if (![_persistentStoreCoordinator removePersistentStore:store error:&error]) {
            secerror("SecCDKeychain: failed to remove persistent store with error: %@", error);
        }
    }

    _classAPersistentStores = nil;
    _managedObjectContext = nil;
    _persistentStoreCoordinator = nil;
}

- (dispatch_queue_t)_queue
{
    return _queue;
}

- (void)performOnManagedObjectQueue:(void (^)(NSManagedObjectContext* context, NSError* error))block
{
    __weak __typeof(self) weakSelf = self;
    dispatch_async(_queue, ^{
        NSError* error = nil;
        NSManagedObjectContext* context = [weakSelf _onQueueGetManagedObjectContextWithError:&error];
        if (context) {
            [context performBlock:^{
                block(context, nil);
            }];
        }
        else {
            if (!error) {
                error = [NSError errorWithDomain:SFKeychainErrorDomain code:SFKeychainErrorInternal description:@"unknown error retrieving managed object context"];
            }
            block(nil, error);
        }
    });
}

- (void)performOnManagedObjectQueueAndWait:(void (^)(NSManagedObjectContext* context, NSError* error))block
{
    dispatch_sync(_queue, ^{
        NSError* error = nil;
        NSManagedObjectContext* context = [self _onQueueGetManagedObjectContextWithError:&error];
        if (context) {
            [context performBlockAndWait:^{
                block(context, nil);
            }];
        }
        else {
            block(nil, error);
        }
    });
}

- (NSString*)primaryKeyNameForItemTypeName:(NSString*)itemTypeName
{
    return [NSString stringWithFormat:@"SecCDKeychain-PrimaryKey-%@", itemTypeName];
}

- (bool)validateItemOwner:(SecCDKeychainAccessControlEntity*)owner withConnection:(SFKeychainServerConnection*)connection withError:(NSError**)error
{
    // in the future we may support more owner types than just an access group, but for now, access group or bust

    NSError* localError = nil;
    NSString* accessGroup = owner.entityType == SecCDKeychainAccessControlEntityTypeAccessGroup ? owner.stringRepresentation : nil;
    if (accessGroup) {
        if ([connection.clientAccessGroups containsObject:accessGroup]) {
            return true;
        }
        else {
            localError = [NSError errorWithDomain:SFKeychainErrorDomain code:SFKeychainErrorInvalidAccessGroup userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"client not in access group: %@", accessGroup]}];
        }
    }
    else {
        localError = [NSError errorWithDomain:SFKeychainErrorDomain code:SFKeychainErrorMissingAccessGroup userInfo:@{NSLocalizedDescriptionKey : @"keychain item missing access group"}];
    }

    if (error) {
        *error = localError;
    }
    secerror("SecCDKeychain: failed to validate item owner with error: %@", localError);
    return false;
}

- (void)insertItems:(NSArray<SecCDKeychainItem*>*)items withConnection:(SFKeychainServerConnection*)connection completionHandler:(void (^)(bool success, NSError* error))completionHandler
{
    __weak __typeof(self) weakSelf = self;
    [self performOnManagedObjectQueue:^(NSManagedObjectContext* managedObjectContext, NSError* managedObjectError) {
        __strong __typeof(self) strongSelf = weakSelf;
        if (!strongSelf) {
            secerror("SecCDKeychain: attempt to insert items into deallocated keychain instance");
            completionHandler(false, [NSError errorWithDomain:SFKeychainErrorDomain code:SFKeychainErrorInternal userInfo:@{NSLocalizedDescriptionKey : @"attempt to insert items into deallocated keychain instance"}]);
            return;
        }

        if (!managedObjectContext) {
            secerror("SecCDKeychain: insertItems: could not get managed object context");
            completionHandler(false, [NSError errorWithDomain:SFKeychainErrorDomain code:SFKeychainErrorInternal userInfo:@{ NSLocalizedDescriptionKey : @"insertItems: could not get managed object context", NSUnderlyingErrorKey : managedObjectError }]);
            return;
        }

        __block NSError* error = nil;
        __block bool success = true;
        for (SecCDKeychainItem* item in items) {
            if (![self validateItemOwner:item.owner withConnection:connection withError:&error]) {
                success = false;
                break;
            }

            NSString* itemTypeName = item.itemType.name;
            NSString* primaryKeyValue = [item primaryKeyStringRepresentationWithError:&error];
            if (primaryKeyValue) {
                NSFetchRequest* primaryKeyFetchRequest = [SecCDKeychainManagedLookupEntry fetchRequest];
                primaryKeyFetchRequest.predicate = [NSPredicate predicateWithFormat:@"lookupKey == %@ AND lookupValue == %@", [self primaryKeyNameForItemTypeName:itemTypeName], primaryKeyValue];
                SecCDKeychainManagedLookupEntry* managedLookupEntry = [[managedObjectContext executeFetchRequest:primaryKeyFetchRequest error:&error] firstObject];
                if (managedLookupEntry) {
                    secerror("SecCDKeychain: failed to unique item (%@) using primary keys: %@", item, item.itemType.primaryKeys);
                    success = false;
                    error = [NSError errorWithDomain:SFKeychainErrorDomain code:SFKeychainErrorDuplicateItem userInfo:@{ NSLocalizedDescriptionKey : [NSString stringWithFormat:@"failed to unique item (%@) using primary keys: %@", item, item.itemType.primaryKeys] }];
                    break;
                }
            }
            else {
                secerror("SecCDKeychain: error creating primary key string representation for item: %@; error: %@", item, error);
                success = false;
                break;
            }

            SecCDKeychainManagedItem* managedItem = [self managedItemWithItem:item withManagedObjectContext:managedObjectContext error:&error];
            if (!managedItem) {
                secerror("SecCDKeychain: error creating managed item for insertion: %@; item: %@", error, item);
                success = false;
                break;
            }

            // add a lookup entry for the primary key
            SecCDKeychainManagedLookupEntry* primaryKeyLookupEntry = [NSEntityDescription insertNewObjectForEntityForName:SecCDKeychainEntityLookupEntry inManagedObjectContext:managedObjectContext];
            primaryKeyLookupEntry.itemTypeName = itemTypeName;
            primaryKeyLookupEntry.lookupKey = [self primaryKeyNameForItemTypeName:itemTypeName];
            primaryKeyLookupEntry.lookupValue = primaryKeyValue;
            primaryKeyLookupEntry.lookupValueType = SecCDKeychainLookupValueTypeString;
            primaryKeyLookupEntry.systemEntry = YES;
            [primaryKeyLookupEntry addMatchingItemsObject:managedItem];
            secdebug("SecCDKeychain", "added primary key: %@ value: %@", primaryKeyLookupEntry.lookupKey, primaryKeyLookupEntry.lookupValue);

            // and add lookup entries for all the things we're supposed to be able to lookup on
            for (SecCDKeychainLookupTuple* lookupTuple in item.lookupAttributes) {
                NSFetchRequest* lookupEntryFetchRequest = [SecCDKeychainManagedLookupEntry fetchRequest];
                lookupEntryFetchRequest.predicate = [NSPredicate predicateWithFormat:@"lookupKey == %@ AND lookupValueType == %@ AND lookupValue == %@", lookupTuple.key, lookupTuple.valueType, lookupTuple.value];
                SecCDKeychainManagedLookupEntry* lookupEntry = [[managedObjectContext executeFetchRequest:lookupEntryFetchRequest error:&error] firstObject];
                if (error) {
                    secerror("SecCDKeychain: error fetching lookup entry during item insertion: %@", (__bridge CFErrorRef)error);
                    success = false;
                    break;
                }
                else if (!lookupEntry) {
                    // we didn't get an error, but we didn't find a lookup entry
                    // that means there simply wasn't one in the db, so we should create one
                    lookupEntry = [NSEntityDescription insertNewObjectForEntityForName:SecCDKeychainEntityLookupEntry inManagedObjectContext:managedObjectContext];
                    lookupEntry.itemTypeName = itemTypeName;
                    lookupEntry.lookupKey = lookupTuple.key;
                    lookupEntry.lookupValue = lookupTuple.stringRepresentation;
                    lookupEntry.lookupValueType = lookupTuple.valueType;
                    secdebug("SecCDKeychain", "added item for key (%@) : value (%@)", lookupEntry.lookupKey, lookupEntry.lookupValue);
                }

                [lookupEntry addMatchingItemsObject:managedItem];
            }

            if (!success) {
                break;
            }
        }

        if (success) {
            secnotice("SecCDKeychain", "saving managed object context for items: %@", items);
            success = [managedObjectContext save:&error];
            if (error) {
                secerror("SecCDKeychain: saving managed object context failed with error: %@", error);
            }
            else {
                secnotice("SecCDKeychain", "saving managed object context succeeded");
            }
        }

        dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
            completionHandler(success, error);
        });
    }];
}

- (SecCDKeychainManagedItem*)fetchManagedItemForPersistentID:(NSUUID*)persistentID withManagedObjectContext:(NSManagedObjectContext*)managedObjectContext error:(NSError**)error
{
    NSError* localError = nil;
    NSFetchRequest* lookupEntryFetchRequest = [SecCDKeychainManagedItem fetchRequest];
    lookupEntryFetchRequest.predicate = [NSPredicate predicateWithFormat:@"persistentID == %@", persistentID];
    SecCDKeychainManagedItem* managedItem = [[managedObjectContext executeFetchRequest:lookupEntryFetchRequest error:&localError] firstObject];
    
    if (error) {
        *error = localError;
    }
    
    return managedItem;
}

- (void)fetchItemForPersistentID:(NSUUID*)persistentID withConnection:(SFKeychainServerConnection*)connection completionHandler:(void (^)(SecCDKeychainItem* item, NSError* error))completionHandler
{
    __weak __typeof(self) weakSelf = self;
    [self performOnManagedObjectQueue:^(NSManagedObjectContext* managedObjectContext, NSError* managedObjectError) {
        __strong __typeof(self) strongSelf = weakSelf;
        if (!strongSelf) {
            secerror("SecCDKeychain: attempt to fetch item from deallocated keychain instance");
            completionHandler(false, [NSError errorWithDomain:SecCDKeychainErrorDomain code:SecCDKeychainErrorInternal userInfo:@{NSLocalizedDescriptionKey : @"attempt to fetch item from deallocated keychain instance"}]);
            return;
        }

        if (!managedObjectContext) {
            secerror("SecCDKeychain: fetchItemForPersistentID: could not get managed object context");
            completionHandler(false, [NSError errorWithDomain:SFKeychainErrorDomain code:SFKeychainErrorInternal userInfo:@{ NSLocalizedDescriptionKey : @"fetchItemForPersistentID: could not get managed object context", NSUnderlyingErrorKey : managedObjectError }]);
            return;
        }
        
        NSError* error = nil;
        SecCDKeychainItem* fetchedItem = nil;
        SecCDKeychainManagedItem* managedItem = [self fetchManagedItemForPersistentID:persistentID withManagedObjectContext:managedObjectContext error:&error];
        if (error) {
            secerror("SecCDKeychain: error fetching item for persistent id: %@; error: %@", persistentID, error);
        }
        else if (!managedItem) {
            // we didn't get an error, but we didn't find an item
            // that means there simply wasn't one in the db, so this lookup finds nothing
            error = [NSError errorWithDomain:SFKeychainErrorDomain code:SFKeychainErrorItemNotFound userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"did not find any keychain items matching persistent ID: %@", persistentID.UUIDString]}];
        }
        else {
            fetchedItem = [[SecCDKeychainItem alloc] initWithManagedItem:managedItem keychain:self error:&error];
            if (!fetchedItem || error) {
                secerror("SecCDKeychain: failed to create SecCDKeychainItem from managed item with error: %@", error);
                fetchedItem = nil;
            }
            else if (![self validateItemOwner:fetchedItem.owner withConnection:connection withError:nil]) { // if we fail owner validation, we don't return an error; we pretend it didn't exist
                fetchedItem = nil;
                error = [NSError errorWithDomain:SFKeychainErrorDomain code:SFKeychainErrorItemNotFound userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"did not find any keychain items matching persistent ID: %@", persistentID.UUIDString]}];
            }
        }
        
        dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
            completionHandler(fetchedItem, error);
        });
    }];
}

- (void)fetchItemsWithValue:(NSString*)value forLookupKey:(NSString*)lookupKey ofType:(SecCDKeychainLookupValueType*)lookupValueType withConnection:(SFKeychainServerConnection*)connection completionHandler:(void (^)(NSArray<SecCDKeychainItemMetadata*>* items, NSError* error))completionHandler
{
    __weak __typeof(self) weakSelf = self;
    [self performOnManagedObjectQueue:^(NSManagedObjectContext* managedObjectContext, NSError* managedObjectError) {
        __strong __typeof(self) strongSelf = weakSelf;
        if (!strongSelf) {
            secerror("SecCDKeychain: attempt to fetch items from deallocated keychain instance");
            completionHandler(false, [NSError errorWithDomain:SecCDKeychainErrorDomain code:SecCDKeychainErrorInternal userInfo:@{NSLocalizedDescriptionKey : @"attempt to lookup items from deallocated keychain instance"}]);
            return;
        }

        if (!managedObjectContext) {
            secerror("SecCDKeychain: fetchItemsWithValue: could not get managed object context");
            completionHandler(false, [NSError errorWithDomain:SFKeychainErrorDomain code:SFKeychainErrorInternal userInfo:@{ NSLocalizedDescriptionKey : @"fetchItemsWithValue: could not get managed object context", NSUnderlyingErrorKey : managedObjectError }]);
            return;
        }

        NSError* error = nil;
        NSMutableArray* fetchedItems = [[NSMutableArray alloc] init];
        NSFetchRequest* lookupEntryFetchRequest = [SecCDKeychainManagedLookupEntry fetchRequest];
        lookupEntryFetchRequest.predicate = [NSPredicate predicateWithFormat:@"lookupKey == %@ AND lookupValueType == %@ AND lookupValue == %@", lookupKey, lookupValueType, value];
        NSArray* lookupEntries = [managedObjectContext executeFetchRequest:lookupEntryFetchRequest error:&error];
        if (error) {
            secerror("SecCDKeychain: error fetching lookup entry during item lookup: %@", error);
            fetchedItems = nil;
        }
        else if (lookupEntries.count == 0) {
            // we didn't get an error, but we didn't find a lookup entry
            // that means there simply wasn't one in the db, so this lookup finds nothing
            CFErrorRef cfError = NULL;
            SecError(errSecItemNotFound, &cfError, CFSTR("did not find any keychain items matching query"));
            error = CFBridgingRelease(cfError);
        }
        else {
            for (SecCDKeychainManagedLookupEntry* lookupEntry in lookupEntries) {
                for (SecCDKeychainManagedItem* item in lookupEntry.matchingItems) {
                    SecCDKeychainItemMetadata* fetchedItem = [[SecCDKeychainItemMetadata alloc] initWithManagedItem:item keychain:self error:&error];
                    if (!fetchedItem || error) {
                        secerror("SecCDKeychain: failed to create SecCDKeychainItemMetadata from managed item with error: %@", error);
                        fetchedItems = nil;
                        break;
                    }
                    else if (![self validateItemOwner:fetchedItem.owner withConnection:connection withError:nil]) { // not an error; just pretend it's not there
                        continue;
                    }

                    [fetchedItems addObject:fetchedItem];
                }
            }
        }
        
        dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
            completionHandler(fetchedItems, error);
        });
    }];
}

- (void)deleteItemWithPersistentID:(NSUUID*)persistentID withConnection:(SFKeychainServerConnection*)connection completionHandler:(void (^)(bool success, NSError* _Nullable error))completionHandler
{
    __weak __typeof(self) weakSelf = self;
    [self performOnManagedObjectQueue:^(NSManagedObjectContext* managedObjectContext, NSError* managedObjectError) {
        __strong __typeof(self) strongSelf = weakSelf;
        if (!strongSelf) {
            secerror("SecCDKeychain: attempt to fetch items from deallocated keychain instance");
            completionHandler(false, [NSError errorWithDomain:SecCDKeychainErrorDomain code:SecCDKeychainErrorInternal userInfo:@{NSLocalizedDescriptionKey : @"attempt to insert items into deallocated keychain instance"}]);
            return;
        }

        if (!managedObjectContext) {
            secerror("SecCDKeychain: deleteItemWithPersistentID: could not get managed object context");
            completionHandler(false, [NSError errorWithDomain:SFKeychainErrorDomain code:SFKeychainErrorInternal userInfo:@{ NSLocalizedDescriptionKey : @"deleteItemWIthPersistentID: could not get managed object context", NSUnderlyingErrorKey : managedObjectError }]);
            return;
        }
        
        NSError* error = nil;
        SecCDKeychainManagedItem* managedItem = [self fetchManagedItemForPersistentID:persistentID withManagedObjectContext:managedObjectContext error:&error];
        bool success = false;
        if (managedItem && !error) {
            SecCDKeychainAccessControlEntity* owner = [[SecCDKeychainAccessControlEntity alloc] initWithManagedEntity:managedItem.owner];
            if ([self validateItemOwner:owner withConnection:connection withError:nil]) { // don't pass error here because we will treat it below as simply "item could not be found"
                for (SecCDKeychainManagedLookupEntry* lookupEntry in managedItem.lookupEntries.copy) {
                    [lookupEntry removeMatchingItemsObject:managedItem];
                    if (lookupEntry.matchingItems.count == 0) {
                        [managedObjectContext deleteObject:lookupEntry];
                    }
                }

                SecCDKeychainManagedAccessControlEntity* managedOwner = managedItem.owner;
                [managedOwner removeOwnedItemsObject:managedItem];
                [managedOwner removeAccessedItemsObject:managedItem];
                if (managedOwner.ownedItems.count == 0 && managedOwner.accessedItems == 0) {
                    [managedObjectContext deleteObject:managedOwner];
                }

                for (SecCDKeychainManagedAccessControlEntity* accessControlEntity in managedItem.accessControlList) {
                    [accessControlEntity removeAccessedItemsObject:managedItem];
                    if (accessControlEntity.ownedItems.count == 0 && accessControlEntity.accessedItems == 0) {
                        [managedObjectContext deleteObject:accessControlEntity];
                    }
                }

                [managedObjectContext deleteObject:managedItem];
                success = [managedObjectContext save:&error];
            }
            else {
                success = false;
            }
        }

        if (!success && !error) {
            secerror("SecCDKeychain: attempt to delete item with persistant identifier that could not be found: %@", persistentID);
            error = [NSError errorWithDomain:SFKeychainErrorDomain code:SFKeychainErrorItemNotFound userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"attempt to delete item with persistant identifier that could not be found: %@", persistentID]}];
        }
        
        dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
            completionHandler(success, error);
        });
    }];
}

- (void)registerItemType:(SecCDKeychainItemType*)itemType withManagedObjectContext:(NSManagedObjectContext*)managedObjectContext
{
    _itemTypeDict[itemType.name] = itemType;

    NSMutableDictionary* itemTypeVersionDict = _managedItemTypeDict[itemType.name];
    if (!itemTypeVersionDict) {
        itemTypeVersionDict = [NSMutableDictionary dictionary];
        _managedItemTypeDict[itemType.name] = itemTypeVersionDict;
    }

    if (!itemTypeVersionDict[@(itemType.version)]) {
        NSError* error = nil;
        SecCDKeychainManagedItemType* managedItemType = [itemType managedItemTypeWithContext:managedObjectContext error:&error];
        if (managedItemType) {
            itemTypeVersionDict[@(itemType.version)] = managedItemType;
            [managedObjectContext save:&error];
        }

        if (!managedItemType || error) {
            secerror("SecCDKeychain: error registering managedItemType for for itemType: %@", itemType);
        }
    }
}

// this method is only for use by tests because built-in types should get registered at initial store setup
- (void)_registerItemTypeForTesting:(SecCDKeychainItemType*)itemType
{
    [self performOnManagedObjectQueueAndWait:^(NSManagedObjectContext* managedObjectContext, NSError* managedObjectError) {
        if (!managedObjectContext) {
            secerror("SecCDKeychain: _registerItemTypeForTesting: could not get managed object context");
            return;
        }

        [self registerItemType:itemType withManagedObjectContext:managedObjectContext];
    }];
}

- (SecCDKeychainItemType*)itemTypeForItemTypeName:(NSString*)itemTypeName
{
    return _itemTypeDict[itemTypeName];
}

- (SecCDKeychainManagedItem*)managedItemWithItem:(SecCDKeychainItem*)item withManagedObjectContext:(NSManagedObjectContext*)managedObjectContext error:(NSError**)error
{
    NSError* localError = nil;
    SecCDKeychainManagedItem* managedItem = [NSEntityDescription insertNewObjectForEntityForName:SecCDKeychainEntityItem inManagedObjectContext:managedObjectContext];
    managedItem.itemType = [[_managedItemTypeDict valueForKey:item.itemType.name] objectForKey:@(item.itemType.version)];
    managedItem.persistentID = item.metadata.persistentID;

    NSData* attributeData = [NSPropertyListSerialization dataWithPropertyList:item.attributes format:NSPropertyListBinaryFormat_v1_0 options:0 error:&localError];
    managedItem.metadata = attributeData;

    SecCDKeychainManagedAccessControlEntity* owner = [NSEntityDescription insertNewObjectForEntityForName:SecCDKeychainEntityTypeAccessControlEntity inManagedObjectContext:managedObjectContext];
    owner.type = (int32_t)item.owner.entityType;
    owner.stringRepresentation = item.owner.stringRepresentation;
    managedItem.owner = owner;
    [owner addOwnedItemsObject:managedItem];
    [owner addAccessedItemsObject:managedItem];

    // today, we only support the device keybag
    // someday, that will have to change
    managedItem.data = [item encryptedSecretDataWithAttributeData:attributeData keybag:KEYBAG_DEVICE error:&localError];

    if (error) {
        *error = localError;
    }

    return localError ? nil : managedItem;
}

@end

@implementation SecCDKeychainItemMetadata {
    SecCDKeychainItemType* _itemType;
    SecCDKeychainAccessControlEntity* _owner;
    NSUUID* _persistentID;
    NSDictionary* _attributes;
    NSSet<SecCDKeychainLookupTuple*>* _lookupAttributes;
    NSData* _managedDataBlob; // hold onto this to verify metadata been tampered with
    keyclass_t _keyclass;
}

@synthesize itemType = _itemType;
@synthesize owner = _owner;
@synthesize persistentID = _persistentID;
@synthesize attributes = _attributes;
@synthesize lookupAttributesSet = _lookupAttributes;
@synthesize managedDataBlob = _managedDataBlob;
@synthesize keyclass = _keyclass;

- (instancetype)initWithItemType:(SecCDKeychainItemType*)itemType persistentID:(NSUUID*)persistentID attributes:(NSDictionary*)attributes lookupAttributes:(NSArray<SecCDKeychainLookupTuple*>*)lookupAttributes owner:(SecCDKeychainAccessControlEntity*)owner keyclass:(keyclass_t)keyclass
{
    if (self = [super init]) {
        _itemType = itemType;
        _owner = owner;
        _persistentID = persistentID.copy;
        _attributes = attributes.copy;
        _keyclass = keyclass;

        if (lookupAttributes) {
            _lookupAttributes = [NSSet setWithArray:lookupAttributes];
        }
        else {
            NSMutableSet* newLookupAttributes = [[NSMutableSet alloc] init];
            [_attributes enumerateKeysAndObjectsUsingBlock:^(NSString* key, id value, BOOL* stop) {
                SecCDKeychainLookupTuple* lookupTuple = [SecCDKeychainLookupTuple lookupTupleWithKey:key value:value];
                [newLookupAttributes addObject:lookupTuple];
            }];
            _lookupAttributes = newLookupAttributes.copy;
        }
    }
    
    return self;
}

- (instancetype)initWithManagedItem:(SecCDKeychainManagedItem*)managedItem keychain:(SecCDKeychain*)keychain error:(NSError**)error
{
    if (self = [super init]) {
        _itemType = [keychain itemTypeForItemTypeName:managedItem.itemType.name];
        _persistentID = managedItem.persistentID.copy;
        _managedDataBlob = managedItem.metadata;
        _attributes = [NSPropertyListSerialization propertyListWithData:_managedDataBlob options:NSPropertyListImmutable format:NULL error:error];
        _owner = [[SecCDKeychainAccessControlEntity alloc] initWithManagedEntity:managedItem.owner];

        NSMutableSet* lookupAttributes = [NSMutableSet set];
        for (SecCDKeychainManagedLookupEntry* lookupEntry in managedItem.lookupEntries) {
            if (!lookupEntry.systemEntry) {
                [lookupAttributes addObject:[SecCDKeychainLookupTuple lookupTupleWithManagedLookupEntry:lookupEntry]];
            }
        }
        _lookupAttributes = lookupAttributes.copy;
        
        if (!_itemType || !_persistentID || !_owner || ![_attributes isKindOfClass:[NSDictionary class]]) {
            if (error) {
                *error = [NSError errorWithDomain:SecCDKeychainErrorDomain code:SecCDKeychainErrorDeserializing userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"failed to deserialize SecCDKeychainItemMetadata with item type (%@) persistentID: %@ owner: %@", _itemType, _persistentID, _owner]}];
            }
            self = nil;;
        }
    }
    
    return self;
}

- (BOOL)isEqual:(SecCDKeychainItemMetadata*)object
{
    return [_itemType isEqual:object.itemType] &&
           [_owner isEqual:object.owner] &&
           [_persistentID isEqual:object.persistentID] &&
           [_attributes isEqual:object.attributes] &&
           [_lookupAttributes isEqual:object.lookupAttributesSet];
}

- (NSString*)description
{
    return [NSString stringWithFormat:@"%@: itemType:(%@) owner:(%@) persistentID:(%@)\n attributes: %@\n lookup attributes: %@", [super description], self.itemType, self.owner, self.persistentID, self.attributes, self.lookupAttributes];
}

- (void)fetchFullItemWithKeychain:(SecCDKeychain*)keychain withConnection:(SFKeychainServerConnection*)connection completionHandler:(void (^)(SecCDKeychainItem* _Nullable item, NSError* _Nullable error))completionHandler
{
    [keychain fetchItemForPersistentID:_persistentID withConnection:connection completionHandler:completionHandler];
}

- (NSArray<SecCDKeychainLookupTuple*>*)lookupAttributes
{
    return _lookupAttributes.allObjects;
}

- (NSArray*)primaryKeys
{
    return _itemType.primaryKeys;
}

@end

@implementation SecCDKeychainItem {
    SecCDKeychainItemMetadata* _metadata;
    NSData* _encryptedSecretData;
    NSDictionary* _secrets;
}

@synthesize metadata = _metadata;
@synthesize secrets = _secrets;

- (instancetype)initItemType:(SecCDKeychainItemType*)itemType withPersistentID:(NSUUID*)persistentID attributes:(NSDictionary*)attributes lookupAttributes:(NSArray<SecCDKeychainLookupTuple*>*)lookupAttributes secrets:(NSDictionary*)secrets owner:(SecCDKeychainAccessControlEntity*)owner keyclass:(keyclass_t)keyclass
{
    if (self = [super init]) {
        _secrets = secrets.copy;
        _metadata = [[SecCDKeychainItemMetadata alloc] initWithItemType:itemType persistentID:persistentID attributes:attributes lookupAttributes:lookupAttributes owner:owner keyclass:keyclass];
        if (!_metadata) {
            self = nil;
        }
    }

    return self;
}

- (instancetype)initWithManagedItem:(SecCDKeychainManagedItem*)managedItem keychain:(SecCDKeychain*)keychain error:(NSError**)error
{
    if (self = [super init]) {
        NSError* localError;
        _metadata = [[SecCDKeychainItemMetadata alloc] initWithManagedItem:(SecCDKeychainManagedItem*)managedItem keychain:keychain error:&localError];
        if (!_metadata) {
            if (error) {
                // TODO: add a negative unit test

                *error = [NSError errorWithDomain:SecCDKeychainErrorDomain code:SecCDKeychainErrorDeserializing userInfo:@{NSLocalizedDescriptionKey : @"could not create SecCDKeychainItem from managed item - managed item was malformed"}];
            }
            return nil;
        }

        _secrets = [self secretsFromEncryptedData:managedItem.data withKeybag:KEYBAG_DEVICE error:&localError];
        if (!_secrets) {
            if (error) {
                *error = [NSError errorWithDomain:SFKeychainErrorDomain code:SFKeychainErrorItemDecryptionFailed userInfo:@{ NSLocalizedDescriptionKey : @"could not decrypt secrets for item", NSUnderlyingErrorKey : localError }];
            }
            return nil;
        }
    }

    return self;
}

- (BOOL)isEqual:(SecCDKeychainItem*)object
{
    return [object isKindOfClass:[SecCDKeychainItem class]]
        && [_metadata isEqual:object.metadata]
        && [_secrets isEqual:object.secrets];
}

- (NSString*)description
{
    return [NSString stringWithFormat:@"%@: itemType:(%@) persistentID:(%@)\n owner: %@\n attributes: %@\n lookup attributes: %@\nprimaryKeys: %@", [super description], self.itemType, self.persistentID, self.owner, self.attributes, self.lookupAttributes, self.primaryKeys];
}

- (SecCDKeychainItemType*)itemType
{
    return _metadata.itemType;
}

- (SecCDKeychainAccessControlEntity*)owner
{
    return _metadata.owner;
}

- (NSUUID*)persistentID
{
    return _metadata.persistentID;
}

- (NSDictionary*)attributes
{
    return _metadata.attributes;
}

- (NSArray<SecCDKeychainLookupTuple*>*)lookupAttributes
{
    return _metadata.lookupAttributes;
}

- (NSArray*)primaryKeys
{
    return _metadata.primaryKeys;
}

- (NSString*)primaryKeyStringRepresentationWithError:(NSError**)error
{
    NSDictionary* attributes = _metadata.attributes;
    NSArray* primaryKeys = _metadata.primaryKeys.count > 0 ? _metadata.primaryKeys : attributes.allKeys;
    NSArray* sortedPrimaryKeys = [primaryKeys sortedArrayUsingComparator:^NSComparisonResult(NSString* firstKey, NSString* secondKey) {
        return [firstKey compare:secondKey options:NSForcedOrderingSearch];
    }];

    SFSHA256DigestOperation* digest = [[SFSHA256DigestOperation alloc] init];
    [digest addData:[_metadata.owner.stringRepresentation dataUsingEncoding:NSUTF8StringEncoding]];

    for (NSString* key in sortedPrimaryKeys) {
        [digest addData:[key dataUsingEncoding:NSUTF8StringEncoding]];

        id value = attributes[key];
        if ([value isKindOfClass:[NSData class]]) {
            [digest addData:value];
        }
        else if ([value isKindOfClass:[NSString class]]) {
            [digest addData:[value dataUsingEncoding:NSUTF8StringEncoding]];
        }
        else {
            NSData* valueData = [NSKeyedArchiver archivedDataWithRootObject:value requiringSecureCoding:YES error:error];
            if (valueData) {
                [digest addData:valueData];
            }
            else {
                return nil;
            }
        }
    }

    return  [[digest hashValue] base64EncodedStringWithOptions:0];
}

- (NSData*)encryptedSecretDataWithAttributeData:(NSData*)attributeData keybag:(keybag_handle_t)keybag error:(NSError**)error
{
#if USE_KEYSTORE
    NSError* localError = nil;
    NSString* errorDescription = nil;

    SFAESKeySpecifier* keySpecifier = [[SFAESKeySpecifier alloc] initWithBitSize:SFAESKeyBitSize256];
    SFAESKey* randomKey = [[SFAESKey alloc] initRandomKeyWithSpecifier:keySpecifier error:&localError];
    if (!randomKey) {
        secerror("SecCDKeychain: failed to create random key for encrypting item with error: %@", localError);
        errorDescription =  @"failed to create random key for encrypting item";
    }

    NSData* itemSecrets = [NSPropertyListSerialization dataWithPropertyList:_secrets format:NSPropertyListBinaryFormat_v1_0 options:0 error:&localError];
    if (!itemSecrets) {
        secerror("SecCDKeychain: failed to serialize item secrets dictionary with error: %@", localError);
        errorDescription = @"failed to serialize item secrets dictionary";
    }

    SFAuthenticatedEncryptionOperation* encryptionOperation = [[SFAuthenticatedEncryptionOperation alloc] initWithKeySpecifier:keySpecifier];
    SFAuthenticatedCiphertext* ciphertext = nil;
    if (randomKey && itemSecrets) {
        NSData* attributeDataSHA256 = [SFSHA256DigestOperation digest:attributeData];
        ciphertext = [encryptionOperation encrypt:itemSecrets withKey:randomKey additionalAuthenticatedData:attributeDataSHA256 error:&localError];
        if (!ciphertext) {
            secerror("SecCDKeychain: failed to encrypt item secret data with error: %@", localError);
            errorDescription = @"failed to encrypt item secret data";
        }
    }

    SecAKSRefKey* refKey = nil;
    NSData* refKeyBlobData =  nil;

    if (ciphertext) {
        refKey = [[SecAKSRefKey alloc] initWithKeybag:keybag keyclass:_metadata.keyclass];
        refKeyBlobData = refKey.refKeyBlob;
        if (!refKey || !refKeyBlobData) {
            secerror("SecCDKeychain: failed to create refKey");
            errorDescription = @"failed to create refKey";
        }
    }

    NSData* wrappedKeyData = nil;
    if (refKey && refKeyBlobData) {
        wrappedKeyData = [refKey wrappedDataForKey:randomKey];
        if (!wrappedKeyData) {
            secerror("SecCDKeychain: failed to encrypt item");
            errorDescription = @"failed to encrypt item";
        }
    }

    if (wrappedKeyData) {
        SecCDKeychainItemWrappedSecretData* wrappedSecretData = [[SecCDKeychainItemWrappedSecretData alloc] initWithCiphertext:ciphertext wrappedKeyData:wrappedKeyData refKeyBlob:refKeyBlobData];
        NSData* wrappedSecretDataBlob = [NSKeyedArchiver archivedDataWithRootObject:wrappedSecretData requiringSecureCoding:YES error:&localError];
        if (wrappedSecretDataBlob) {
            return wrappedSecretDataBlob;
        }
        else {
            secerror("SecCDKeychain: failed to serialize item secret data blob with error: %@", localError);
            errorDescription = @"failed to serialize item secret data blob";
        }
    }

    if (error) {
        NSDictionary* userInfo = localError ? @{ NSUnderlyingErrorKey : localError, NSLocalizedDescriptionKey : errorDescription } : @{NSLocalizedDescriptionKey : errorDescription};
        *error = [NSError errorWithDomain:SecCDKeychainErrorDomain code:SecCDKeychainErrorInternal userInfo:userInfo];
    }

#endif
    return nil;
}

- (NSDictionary*)secretsFromEncryptedData:(NSData*)secretData withKeybag:(keybag_handle_t)keybag error:(NSError**)error
{
#if USE_KEYSTORE
    SecCDKeychainItemWrappedSecretData* wrappedSecretData = [NSKeyedUnarchiver unarchivedObjectOfClass:[SecCDKeychainItemWrappedSecretData class] fromData:secretData error:error];
    if (!wrappedSecretData) {
        secerror("SecCDKeychain: failed to deserialize item wrapped secret data");
        return nil;
    }

    SecAKSRefKey* refKey = [[SecAKSRefKey alloc] initWithBlob:wrappedSecretData.refKeyBlob keybag:keybag];
    if (!refKey) {
        secerror("SecCDKeychain: failed to create refKey for unwrapping item secrets");
        if (error) {
            *error = [NSError errorWithDomain:SecCDKeychainErrorDomain code:SecCDKeychainErrorInternal userInfo:@{NSLocalizedDescriptionKey : @"failed to create refKey for unwrapping item secrets"}];
        }
        return nil;
    }

    SFAESKey* key = [refKey keyWithWrappedData:wrappedSecretData.wrappedKeyData];
    if (!key) {
        secerror("SecCDKeychain: failed to create item key for decryption");
        return nil;
    }

    SFAESKeySpecifier* keySpecifier = [[SFAESKeySpecifier alloc] initWithBitSize:SFAESKeyBitSize256];
    SFAuthenticatedEncryptionOperation* encryptionOperation = [[SFAuthenticatedEncryptionOperation alloc] initWithKeySpecifier:keySpecifier];
    NSData* metadataSHA256 = [SFSHA256DigestOperation digest:_metadata.managedDataBlob];
    NSData* decryptedSecretData = [encryptionOperation decrypt:wrappedSecretData.ciphertext withKey:key additionalAuthenticatedData:metadataSHA256 error:error];
    if (!decryptedSecretData) {
        secerror("SecCDKeychain: failed to decrypt item secret data");
        return nil;
    }
    
    NSDictionary* secrets = [NSPropertyListSerialization propertyListWithData:decryptedSecretData options:0 format:NULL error:error];
    if (![secrets isKindOfClass:[NSDictionary class]]) {
        secerror("SecCDKeychain: failed to deserialize item decrypted secret data");
        return nil;
    }
    
    return secrets;
#else
    return nil;
#endif
}

// TODO: get back to this as part of CKKS integration work

//- (NSDictionary*)attributesPropertyListWithError:(NSError**)error
//{
//    __block NSDictionary* (^dictionaryToPropertyList)(NSDictionary*) = NULL;
//    __block NSArray* (^arrayToPropertyList)(NSArray*) = NULL;
//
//    dictionaryToPropertyList = ^(NSDictionary* dict) {
//        NSMutableDictionary* propertyList = [NSMutableDictionary dictionary];
//        [dict enumerateKeysAndObjectsUsingBlock:^(NSString* key, id object, BOOL* stop) {
//            Class objectClass = [object class];
//            if ([objectClass isKindOfClass:[NSString class]] || [objectClass isKindOfClass:[NSData class]] || [objectClass isKindOfClass:[NSDate class]] || [objectClass isKindOfClass:[NSNumber class]]) {
//                propertyList[key] = object;
//            }
//            else if ([objectClass isKindOfClass:[NSDictionary class]]){
//                NSDictionary* objectAsPropertyList = dictionaryToPropertyList(object);
//                if (objectAsPropertyList) {
//                    propertyList[key] = objectAsPropertyList;
//                }
//                else {
//                    *stop = YES;
//                }
//            }
//            else if ([objectClass isKindOfClass:[NSArray class]]) {
//                NSArray* objectAsPropertyList = arrayToPropertyList(object);
//                if (objectAsPropertyList) {
//                    propertyList[key] = objectAsPropertyList;
//                }
//                else {
//                    *stop = YES;
//                }
//            }
//            else if ([object conformsToProtocol:@protocol(NSSecureCoding)]) {
//                NSData*
//            }
//        }];
//    }
//}

@end

@implementation SecCDKeychainLookupTuple {
    NSString* _key;
    id<NSCopying, NSObject> _value;
    SecCDKeychainLookupValueType* _valueType;
}

@synthesize key = _key;
@synthesize value = _value;
@synthesize valueType = _valueType;

+ (instancetype)lookupTupleWithKey:(NSString*)key value:(id<NSCopying, NSObject>)value
{
    return [[self alloc] initWithKey:key value:value];
}

+ (instancetype)lookupTupleWithManagedLookupEntry:(SecCDKeychainManagedLookupEntry*)lookupEntry
{
    NSString* valueString = lookupEntry.lookupValue;
    id value;
    NSString* valueType = lookupEntry.lookupValueType;
    if ([valueType isEqualToString:SecCDKeychainLookupValueTypeString]) {
        value = valueString;
    }
    else {
        NSData* valueData = [[NSData alloc] initWithBase64EncodedString:valueString options:0];

        if ([valueType isEqualToString:SecCDKeychainLookupValueTypeData]) {
            value = valueData;
        }
        else if ([valueType isEqualToString:SecCDKeychainLookupValueTypeDate]) {
            // TODO: error parameter
            value = [NSKeyedUnarchiver unarchivedObjectOfClass:[NSDate class] fromData:valueData error:nil];
        }
        else if ([valueType isEqualToString:SecCDKeychainLookupValueTypeNumber]) {
            value = [NSKeyedUnarchiver unarchivedObjectOfClass:[NSNumber class] fromData:valueData error:nil];
        }
        else {
            // TODO: error here
            value = nil;
        }
    }

    return value ? [[self alloc] initWithKey:lookupEntry.lookupKey value:value] : nil;
}

- (instancetype)initWithKey:(NSString*)key value:(id<NSCopying, NSObject>)value
{
    if (self = [super init]) {
        SecCDKeychainLookupValueType* valueType = [SecCDKeychain lookupValueTypeForObject:value];
        BOOL zeroLengthValue = ([valueType isEqualToString:SecCDKeychainLookupValueTypeString] && [(NSString*)value length] == 0) || ([valueType isEqualToString:SecCDKeychainLookupValueTypeData] && [(NSData*)value length] == 0);
        if (valueType && !zeroLengthValue) {
            _key = key.copy;
            _value = [value copyWithZone:nil];
            _valueType = valueType.copy;
        }
        else {
            // TODO: add an error parameter to this method
            self = nil;
        }
    }

    return self;
}

- (BOOL)isEqual:(SecCDKeychainLookupTuple*)object
{
    return [_key isEqualToString:object.key] && [_value isEqual:object.value] && [_valueType isEqualToString:object.valueType];
}

- (NSUInteger)hash
{
    return _key.hash ^ _value.hash ^ _valueType.hash;
}

- (NSString*)description
{
    return [NSString stringWithFormat:@"%@ : %@ [%@]", _key, _value, _valueType];
}

- (NSString*)stringRepresentation
{
    if ([_valueType isEqualToString:SecCDKeychainLookupValueTypeString]) {
        return (NSString*)_value;
    }
    else if ([_valueType isEqualToString:SecCDKeychainLookupValueTypeData]) {
        return [(NSData*)_value base64EncodedStringWithOptions:0];
    }
    else {
        return [[NSKeyedArchiver archivedDataWithRootObject:_value requiringSecureCoding:YES error:nil] base64EncodedStringWithOptions:0];
    }
}

@end

@implementation SecCDKeychainItemType {
    NSString* _name;
    int32_t _version;
    NSSet* _primaryKeys;
    NSSet* _syncableKeys;
    SecCDKeychainManagedItemType* _managedItemType;
}

@synthesize name = _name;
@synthesize version = _version;

+ (instancetype)itemType
{
    return nil;
}

+ (nullable instancetype)itemTypeForVersion:(int32_t)version
{
    return [self itemType];
}

- (instancetype)_initWithName:(NSString*)name version:(int32_t)version primaryKeys:(NSArray*)primaryKeys syncableKeys:(NSArray*)syncableKeys
{
    if (self = [super init]) {
        _name = name.copy;
        _version = version;
        _primaryKeys = [NSSet setWithArray:primaryKeys];
        _syncableKeys = [NSSet setWithArray:syncableKeys];
    }

    return self;
}

- (BOOL)isEqual:(SecCDKeychainItemType*)object
{
    return [object isKindOfClass:[SecCDKeychainItemType class]] &&
           [_name isEqualToString:object.name] &&
           _version == object.version &&
           [_primaryKeys isEqualToSet:object->_primaryKeys] &&
           [_syncableKeys isEqualToSet:object->_syncableKeys];
}

- (NSString*)description
{
    return [NSString stringWithFormat:@"%@ | %d", _name, _version];
}

- (NSString*)debugDescription
{
    return [NSString stringWithFormat:@"%@\n name: %@\n version: %d\n primaryKeys: %@\n syncableKeys: %@", [super debugDescription], _name, _version, _primaryKeys, _syncableKeys];
}

- (NSArray*)primaryKeys
{
    return _primaryKeys.allObjects;
}

- (NSArray*)syncableKeys
{
    return _syncableKeys.allObjects;
}

- (SecCDKeychainManagedItemType*)managedItemTypeWithContext:(NSManagedObjectContext*)managedObjectContext error:(NSError**)error
{
    NSError* localError = nil;
    SecCDKeychainManagedItemType* managedItemType = [NSEntityDescription insertNewObjectForEntityForName:SecCDKeychainEntityItemType inManagedObjectContext:managedObjectContext];
    managedItemType.name = _name;
    managedItemType.version = _version;
    managedItemType.primaryKeys = [NSPropertyListSerialization dataWithPropertyList:_primaryKeys.allObjects format:NSPropertyListBinaryFormat_v1_0 options:0 error:&localError];
    managedItemType.syncableKeys = [NSPropertyListSerialization dataWithPropertyList:_syncableKeys.allObjects format:NSPropertyListBinaryFormat_v1_0 options:0 error:&localError];

    if (error) {
        *error = localError;
    }

    return localError ? nil : managedItemType;
}

@end

@implementation SecCDKeychainAccessControlEntity {
    SecCDKeychainAccessControlEntityType _entityType;
    NSString* _stringRepresentation;
}

@synthesize entityType = _entityType;
@synthesize stringRepresentation = _stringRepresentation;

+ (instancetype)accessControlEntityWithType:(SecCDKeychainAccessControlEntityType)type stringRepresentation:(NSString*)stringRepresentation
{
    return [[self alloc] _initWithEntityType:type stringRepresentation:stringRepresentation];
}

- (instancetype)_initWithEntityType:(SecCDKeychainAccessControlEntityType)type stringRepresentation:(NSString*)stringRepresentation
{
    if (self = [super init]) {
        _entityType = type;
        _stringRepresentation = stringRepresentation.copy;
    }

    return self;
}

- (instancetype)initWithManagedEntity:(SecCDKeychainManagedAccessControlEntity*)managedAccessControlEntity
{
    return [self _initWithEntityType:managedAccessControlEntity.type stringRepresentation:managedAccessControlEntity.stringRepresentation];
}

- (BOOL)isEqual:(SecCDKeychainAccessControlEntity*)object
{
    return _entityType == object.entityType && [_stringRepresentation isEqualToString:object.stringRepresentation];
}

@end

#endif // TARGET_OS_BRIDGE
