//
//  MYKeychainItem.m
//  MYCrypto
//
//  Created by Jens Alfke on 3/26/09.
//  Copyright 2009 Jens Alfke. All rights reserved.
//

#import "MYKeychainItem.h"
#import "MYCrypto_Private.h"
#import "MYErrorUtils.h"


NSString* const MYCSSMErrorDomain = @"CSSMErrorDomain";


@implementation MYKeychainItem


- (id) initWithKeychainItemRef: (MYKeychainItemRef)itemRef;
{
    Assert(itemRef!=NULL);
    self = [super init];
    if (self != nil) {
        _itemRef = itemRef;
        CFRetain(_itemRef);
    }
    return self;
}


@synthesize keychainItemRef=_itemRef;

- (void) dealloc
{
    if (_itemRef) CFRelease(_itemRef);
    [super dealloc];
}

- (void) finalize
{
    if (_itemRef) CFRelease(_itemRef);
    [super finalize];
}

- (id) copyWithZone: (NSZone*)zone {
    // As keys are immutable, it's not necessary to make copies of them. This makes it more efficient
    // to use instances as NSDictionary keys or store them in NSSets.
    return [self retain];
}

- (BOOL) isEqual: (id)obj {
    return (obj == self) ||
           ([obj isKindOfClass: [MYKeychainItem class]] && CFEqual(_itemRef, [obj keychainItemRef]));
}

- (NSUInteger) hash {
    return CFHash(_itemRef);
}

- (NSString*) description {
    return $sprintf(@"%@[%p]", [self class], _itemRef);     //FIX: Can we do anything better?
}


- (NSArray*) _itemList {
    return $array((id)_itemRef);
}

#if MYCRYPTO_USE_IPHONE_API
- (CFDictionaryRef) asQuery {
    return (CFDictionaryRef) $dict( {(id)kSecClass, (id)kSecClassKey},//FIX
                                    {(id)kSecMatchItemList, self._itemList} );
}
#endif


- (MYKeychain*) keychain {
#if MYCRYPTO_USE_IPHONE_API
    return [MYKeychain defaultKeychain];
#else
    MYKeychain *keychain = nil;
    SecKeychainRef keychainRef = NULL;
    if (check(SecKeychainItemCopyKeychain((SecKeychainItemRef)_itemRef, &keychainRef), @"SecKeychainItemCopyKeychain")) {
        if (keychainRef) {
            keychain = [[[MYKeychain alloc] initWithKeychainRef: keychainRef] autorelease];
            CFRelease(keychainRef);
        }
    }
    return keychain;
#endif
}

- (BOOL) removeFromKeychain {
    OSStatus err;
#if MYCRYPTO_USE_IPHONE_API
    err = SecItemDelete(self.asQuery);
#else
    err = SecKeychainItemDelete((SecKeychainItemRef)_itemRef);
#endif
    return err==errSecItemNotFound || check(err, @"SecKeychainItemDelete");
}


#pragma mark -
#pragma mark DATA / METADATA ACCESSORS:


- (NSData*) _getContents: (OSStatus*)outError {
    NSData *contents = nil;
#if MYCRYPTO_USE_IPHONE_API
#else
	UInt32 length = 0;
    void *bytes = NULL;
    *outError = SecKeychainItemCopyAttributesAndData(_itemRef, NULL, NULL, NULL, &length, &bytes);
    if (!*outError && bytes) {
        contents = [NSData dataWithBytes: bytes length: length];
        SecKeychainItemFreeAttributesAndData(NULL, bytes);
    }
#endif
    return contents;
}

+ (NSData*) _getAttribute: (SecKeychainAttrType)attr ofItem: (MYKeychainItemRef)item {
    NSData *value = nil;
#if MYCRYPTO_USE_IPHONE_API
    NSDictionary *info = $dict( {(id)kSecClass, (id)kSecClassKey},
                                {(id)kSecMatchItemList, $array((id)item)},
                                {(id)kSecReturnAttributes, $true} );
    CFDictionaryRef attrs;
    if (!check(SecItemCopyMatching((CFDictionaryRef)info, (CFTypeRef*)&attrs), @"SecItemCopyMatching"))
        return nil;
    CFTypeRef rawValue = CFDictionaryGetValue(attrs,attr);
    value = rawValue ?[[(id)CFMakeCollectable(rawValue) retain] autorelease] :nil;
    CFRelease(attrs);
    
#else
	UInt32 format = kSecFormatUnknown;
	SecKeychainAttributeInfo info = {.count=1, .tag=(UInt32*)&attr, .format=&format};
    SecKeychainAttributeList *list = NULL;
	
    if (check(SecKeychainItemCopyAttributesAndData((SecKeychainItemRef)item, &info,
                                                   NULL, &list, NULL, NULL),
              @"SecKeychainItemCopyAttributesAndData")) {
        if (list) {
            if (list->count == 1)
                value = [NSData dataWithBytes: list->attr->data
                                       length: list->attr->length];
            else if (list->count > 1)
                Warn(@"Multiple values for keychain item attribute");
            SecKeychainItemFreeAttributesAndData(list, NULL);
        }
    }
#endif
    return value;
}

+ (NSString*) _getStringAttribute: (SecKeychainAttrType)attr ofItem: (MYKeychainItemRef)item {
    NSData *value = [self _getAttribute: attr ofItem: item];
    if (!value) return nil;
    const char *bytes = value.bytes;
    size_t length = value.length;
    if (length>0 && bytes[length-1] == 0)
        length--;           // Some values are null-terminated!?
    NSString *str = [[NSString alloc] initWithBytes: bytes length: length
                                           encoding: NSUTF8StringEncoding];
    if (!str)
        Warn(@"MYKeychainItem: Couldn't decode attr value as string");
    return [str autorelease];
}

- (NSString*) stringValueOfAttribute: (SecKeychainAttrType)attr {
    return [[self class] _getStringAttribute: attr ofItem: _itemRef];
}


+ (BOOL) _setAttribute: (SecKeychainAttrType)attr ofItem: (MYKeychainItemRef)item
           stringValue: (NSString*)stringValue
{
#if MYCRYPTO_USE_IPHONE_API
    id value = stringValue ?(id)stringValue :(id)[NSNull null];
    NSDictionary *query = $dict({(id)kSecClass, (id)kSecClassKey},
                                {(id)kSecAttrKeyType, (id)attr},
                                {(id)kSecMatchItemList, $array((id)item)});
    NSDictionary *attrs = $dict({(id)attr, value});
    return check(SecItemUpdate((CFDictionaryRef)query, (CFDictionaryRef)attrs), @"SecItemUpdate");
    
#else
    NSData *data = [stringValue dataUsingEncoding: NSUTF8StringEncoding];
    SecKeychainAttribute attribute = {.tag=attr, .length=data.length, .data=(void*)data.bytes};
	SecKeychainAttributeList list = {.count=1, .attr=&attribute};
    return check(SecKeychainItemModifyAttributesAndData((SecKeychainItemRef)item, &list, 0, NULL),
                 @"SecKeychainItemModifyAttributesAndData");
#endif
}

- (BOOL) setValue: (NSString*)valueStr ofAttribute: (SecKeychainAttrType)attr {
    return [[self class] _setAttribute: attr ofItem: _itemRef stringValue: valueStr];
}


@end




BOOL check(OSStatus err, NSString *what) {
    if (err) {
#if !MYCRYPTO_USE_IPHONE_API
        if (err < -2000000000)
            return checkcssm(err,what);
#endif
        Warn(@"MYCrypto error, %@: %@", what, MYErrorName(NSOSStatusErrorDomain,err));
        return NO;
    } else
        return YES;
}

#if !MYCRYPTO_USE_IPHONE_API
BOOL checkcssm(CSSM_RETURN err, NSString *what) {
    if (err != CSSM_OK) {
        Warn(@"MYCrypto error, %@: %@", what, MYErrorName(MYCSSMErrorDomain,err));
        return NO;
    } else
        return YES;
}
#endif
