// Copyright 2001-2002 Omni Development, Inc.  All rights reserved.
//
// This software may only be used and reproduced according to the
// terms in the file OmniSourceLicense.html, which should be
// distributed with this project and can also be found at
// http://www.omnigroup.com/DeveloperResources/OmniSourceLicense.html.

#import "OWAuthorization-KeychainFunctions.h"

#import <Foundation/Foundation.h>
#import <CoreServices/CoreServices.h>
#import <OmniBase/OmniBase.h>
#import <OmniFoundation/OmniFoundation.h>

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OWF/Processors.subproj/Protocols.subproj/OWAuthorization-KeychainFunctions.m,v 1.9 2002/03/09 01:53:52 kc Exp $");

/* This'll be big enough for practically everything. On the off chance it's too small, we try again. */
/* NOTE: KCGetAttribute() will do invisible silent *format conversions* on some attributes if you say you have an 8-byte buffer! What was Apple smoking? */
#define INITIAL_TRY_LENGTH 128  /* change to 9 or so for debug */

NSData *OWKCGetItemAttribute(KCItemRef item, KCItemAttr attrTag)
{
    SecKeychainAttribute attr;
    OSStatus keychainStatus;
    UInt32 actualLength;
    void *freeMe = NULL;
    
    attr.tag = attrTag;
    actualLength = INITIAL_TRY_LENGTH;
    attr.length = actualLength;  /* KCGetAttribute() doesn't appear to write this field, at least in Cheetah4K29, but it may read it */
    attr.data = alloca(actualLength);
        
    keychainStatus = KCGetAttribute(item, &attr, &actualLength);
    if (keychainStatus == errKCBufferTooSmall) {
        /* the attribute length will have been placed into actualLength */
        freeMe = NSZoneMalloc(NULL, actualLength);
        attr.length = actualLength;
        attr.data = freeMe;
        keychainStatus = KCGetAttribute(item, &attr, &actualLength);
    }
    if (keychainStatus == noErr) {
        NSData *retval = [NSData dataWithBytes:attr.data length:actualLength];
	if (freeMe != NULL)
            NSZoneFree(NULL, freeMe);
        // NSLog(@"attr '%c%c%c%c' value %@", ((char *)&attrTag)[0], ((char *)&attrTag)[1], ((char *)&attrTag)[2], ((char *)&attrTag)[3], retval);
        return retval;
    }
    
    if (freeMe != NULL)
        NSZoneFree(NULL, freeMe);

    if (keychainStatus == errKCNoSuchAttr) {
        /* An expected error. Return nil for nonexistent attributes. */
        return nil;
    }
    
    /* An unexpected error, probably indicating a real problem. Raise an exception. */
    [NSException raise:@"Keychain error" format:@"Error number %d occurred while trying to fetch an item attribute, and Apple's too stingy to include a strerror() equivalent.", keychainStatus];
    
    return nil;  // appease the dread compiler warning gods
}

#if 0

#warning using private API
// KCGetData() is in Carbon, which we don't want to link against in OWF. It has a non-UI counterpart in CoreServices called KCGetDataNoUI(), but it's not declared.
extern OSStatus 
KCGetDataNoUI(
  KCItemRef   item,
  UInt32      maxLength,
  void *      data,
  UInt32 *    actualLength);

#else

// KCGetDataNoUI() disappeared in a recent (post-10.0.4) build at Apple, so we replace it with a stub function which always fails. OmniWeb overrides this function, so this won't affect OmniWeb. But it does mean that other users of OWF will be unable to perform authorization (unless they also override the function)

static OSStatus getDataNoUIStub(
  KCItemRef   item,
  UInt32      maxLength,
  void *      data,
  UInt32 *    actualLength)
{
    if (actualLength)
        *actualLength = 0;
    
    NSLog(@"warning: KCGetDataNoUI() no longer available; noninteractive apps can't use keychain");
    
    return errKCInteractionNotAllowed;
}

#define KCGetDataNoUI getDataNoUIStub

static OSStatus addItemNoUIStub(KCItemRef item)
{
    NSLog(@"warning: KCAddItemNoUI() no longer available; noninteractive apps can't use keychain");

    return errKCInteractionNotAllowed;
}

#endif


OSStatus OWKCExtractKeyData(KCItemRef item, NSData **password, void *funcOverride)
{
    OSStatus keychainStatus;
    UInt32 actualLength;
    char shortBuf[INITIAL_TRY_LENGTH];
    void *freeMe, *buffer;
    OSStatus (*getDataFunc)(KCItemRef, UInt32, void *, UInt32 *);
    
    // This bit of silliness is to allow OWF to remain innocent of all GUI functionality (such as the unlock / password dialogues popped up by KCGetData()) but to allow higher-level frameworks to plug in a more interactive routine.
    if (funcOverride)
        getDataFunc = funcOverride;
    else
        getDataFunc = KCGetDataNoUI;

    freeMe = NULL;
    actualLength = INITIAL_TRY_LENGTH;
    buffer = shortBuf;
    keychainStatus = getDataFunc(item, actualLength, buffer, &actualLength);
    if (keychainStatus == errKCBufferTooSmall) {
        freeMe = NSZoneMalloc(NULL, actualLength);
        buffer = freeMe;
        keychainStatus = getDataFunc(item, actualLength, buffer, &actualLength);
    }
    if (keychainStatus == noErr) {
        *password = [NSData dataWithBytes:buffer length:actualLength];
    }
    if (freeMe)
        NSZoneFree(NULL, freeMe);
    return keychainStatus;
}

static inline NSString *parseKeychainString(NSData *strbytes) {
#warning Keychain API does not yet support multiple encodings, using default C string encoding for now
    // Once Keychain Access supports multiple encodings, it will probably stash a ScriptCode into the keychain item. We'll be able to use that ScriptCode to convert item attributes correctly.
    // Trim trailing nulls here, since some strings have 'em and some don't. This could break multibyte encodings, so when we fix those (see above) we'll have to change this as well.
    if ([strbytes length] && (((char *)[strbytes bytes])[[strbytes length]-1] == 0))
        strbytes = [strbytes subdataWithRange:NSMakeRange(0, [strbytes length]-1)];
    return [[[NSString alloc] initWithData:strbytes encoding:[NSString defaultCStringEncoding]] autorelease];
}

#if 0
static NSDate *parseKeychainDate(NSData *date)
{
    NSString *text;
    NSDate *retval;
    
    /* The documentation states that keychain creation and mod-time dates are UInt32, but in fact they appear to be ASCII strings in a packed calendar format. */
    if ([date length] <= 8) {
        /* Are these Unix-style seconds-since-1970, or Mac-style seconds-since-1900 ? Nobody knows. */
        [NSException raise:@"Keychain error" format:@"Unexpected timestamp format in keychain item."];
    }
    
    text = parseKeychainString(date);
    retval = [NSCalendarDate dateWithString:text calendarFormat:@"%Y%m%d%H%M%SZ"];
    if (!retval) {
        NSLog(@"Couldn't convert date: %@", text);
        return [NSDate distantPast];
    }
    return retval;
}
#endif

static NSNumber *parseKeychainInteger(NSData *value) {
    // Endianness? Portability? Bah! We don't need no portability! Everything's a Macintosh now!
    UInt32 int4 = 0;
    UInt16 int2 = 0;
    UInt8 int1 = 0;
    
    switch([value length]) {
        case 4:
            [value getBytes:&int4];
            break;
        case 2:
            [value getBytes:&int2];
            int4 = int2;
            break;
        case 1:
            [value getBytes:&int1];
            int4 = int1;
            break;
        default:
            [NSException raise:@"Keychain error" format:@"Unexpected integer format in keychain item."];
    }
    
    return [NSNumber numberWithUnsignedInt:int4];
}

NSMutableDictionary *OWKCExtractKeyAttributes(KCItemRef itemRef)
{
    NSMutableDictionary *dict = [[[NSMutableDictionary alloc] initWithCapacity:13] autorelease];
    NSData *val;
    
    val = OWKCGetItemAttribute(itemRef, kClassKCItemAttr);
    if (val) [dict setObject:val forKey:@"class"];

    val = OWKCGetItemAttribute(itemRef, kCreationDateKCItemAttr);
    if (val) [dict setObject:val forKey:@"creationDate"];
    
    val = OWKCGetItemAttribute(itemRef, kModDateKCItemAttr);
    if (val) [dict setObject:val forKey:@"modDate"];
    
    val = OWKCGetItemAttribute(itemRef, kDescriptionKCItemAttr);
    if (val && [val length]) [dict setObject:parseKeychainString(val) forKey:@"description"];
    
    val = OWKCGetItemAttribute(itemRef, kCommentKCItemAttr);
    if (val && [val length]) [dict setObject:parseKeychainString(val) forKey:@"comment"];
    
    val = OWKCGetItemAttribute(itemRef, kLabelKCItemAttr);
    if (val && [val length]) [dict setObject:parseKeychainString(val) forKey:@"label"];
    
    val = OWKCGetItemAttribute(itemRef, kAccountKCItemAttr);
    if (val) [dict setObject:parseKeychainString(val) forKey:@"account"];

    val = OWKCGetItemAttribute(itemRef, kServiceKCItemAttr);
    if (val) [dict setObject:parseKeychainString(val) forKey:@"service"];

    val = OWKCGetItemAttribute(itemRef, kSecurityDomainKCItemAttr);
    if (val && [val length]) [dict setObject:parseKeychainString(val) forKey:@"securityDomain"];

    val = OWKCGetItemAttribute(itemRef, kServerKCItemAttr);
    if (val) [dict setObject:parseKeychainString(val) forKey:@"server"];

    val = OWKCGetItemAttribute(itemRef, kAuthTypeKCItemAttr);
    if (val) [dict setObject:val forKey:@"authType"];

    val = OWKCGetItemAttribute(itemRef, kProtocolKCItemAttr);
    if (val) [dict setObject:val forKey:@"protocol"];

    val = OWKCGetItemAttribute(itemRef, kPortKCItemAttr);
    if (val && [val length]) [dict setObject:parseKeychainInteger(val) forKey:@"port"];

    val = OWKCGetItemAttribute(itemRef, kPathKCItemAttr);
    if (val && [val length]) [dict setObject:parseKeychainString(val) forKey:@"path"];

    return dict;
}

static NSData *formatKeychain4CC(id value)
{
    // catch NSStrings containing decimal numbers (bleah)
    if ([value isKindOfClass:[NSString class]] && (([value length] == 0) || ([value length] != 4 && [value unsignedIntValue] != 0))) {
        value = [NSNumber numberWithUnsignedInt:[value unsignedIntValue]];
    }

    if ([value isKindOfClass:[NSData class]])
        return value;
    if ([value isKindOfClass:[NSNumber class]]) {
        UInt32 aCode = [value unsignedIntValue];
        return [NSData dataWithBytes:&aCode length:sizeof(aCode)];
    }
    
    return [value dataUsingEncoding:[NSString defaultCStringEncoding]];
}

static NSData *formatKeychainString(id value)
{
    return [value dataUsingEncoding:[NSString defaultCStringEncoding]];
}

static NSData *formatKeychainInteger(id value)
{
    UInt32 int4;
    
    int4 = [value intValue];
    return [NSData dataWithBytes:&int4 length:sizeof(int4)];
}

static SecKeychainAttribute *KeychainAttributesFromDictionary(NSDictionary *params, UInt32 *returnAttributeCount)
{
    SecKeychainAttribute *attributes;
    unsigned attributeCount, attributeIndex;
    NSEnumerator *paramNameEnumerator;
    NSString *paramName;

    OBPRECONDITION(returnAttributeCount != NULL);
    attributeCount = [params count];
    attributes = malloc(sizeof(*attributes) * attributeCount);
    attributeIndex = 0;

    paramNameEnumerator = [params keyEnumerator];
    while ( (paramName = [paramNameEnumerator nextObject]) != nil) {
        id paramValue = [params objectForKey:paramName];
        NSData *data;

        if ([paramName isEqualToString:@"class"]) {
            attributes[attributeIndex].tag = kClassKCItemAttr;
            data = formatKeychain4CC(paramValue);
        } else if ([paramName isEqualToString:@"description"]) {
            attributes[attributeIndex].tag = kDescriptionKCItemAttr;
            data = formatKeychainString(paramValue);
        } else if ([paramName isEqualToString:@"account"]) {
            attributes[attributeIndex].tag = kAccountKCItemAttr;
            data = formatKeychainString(paramValue);
        } else if ([paramName isEqualToString:@"service"]) {
            attributes[attributeIndex].tag = kServiceKCItemAttr;
            data = formatKeychainString(paramValue);
        } else if ([paramName isEqualToString:@"securityDomain"]) {
            attributes[attributeIndex].tag = kSecurityDomainKCItemAttr;
            data = formatKeychainString(paramValue);
        } else if ([paramName isEqualToString:@"server"]) {
            attributes[attributeIndex].tag = kServerKCItemAttr;
            data = formatKeychainString(paramValue);
        } else if ([paramName isEqualToString:@"authType"]) {
            attributes[attributeIndex].tag = kAuthTypeKCItemAttr;
            data = formatKeychain4CC(paramValue);
        } else if ([paramName isEqualToString:@"port"]) {
            attributes[attributeIndex].tag = kPortKCItemAttr;
            data = formatKeychainInteger(paramValue);
        } else if ([paramName isEqualToString:@"path"]) {
            attributes[attributeIndex].tag = kPathKCItemAttr;
            data = formatKeychainString(paramValue);
        } else if ([paramName isEqualToString:@"protocol"]) {
            attributes[attributeIndex].tag = kProtocolKCItemAttr;
            data = formatKeychain4CC(paramValue);
        } else {
            // this shouldn't happen.
            continue;
        }

        attributes[attributeIndex].length = [data length];
        attributes[attributeIndex].data = (char *)[data bytes];
        attributeIndex++;
    }
    *returnAttributeCount = attributeIndex;
    return attributes;
}

OSStatus OWKCBeginKeychainSearch(KCRef chain, NSDictionary *params, KCSearchRef *grepstate, KCItemRef *firstitem)
{
    SecKeychainAttributeList attributeList;
    OSStatus keychainStatus;

    if (!params) {
	return KCFindFirstItem(chain, NULL, grepstate, firstitem);
    }
    attributeList.attr = KeychainAttributesFromDictionary(params, &attributeList.count);
    keychainStatus = KCFindFirstItem(chain, &attributeList, grepstate, firstitem);
    free(attributeList.attr);
    return keychainStatus; 
}

OWF_PRIVATE_EXTERN OSStatus OWKCUpdateInternetPassword(NSString *hostname, NSString *realm, NSString *username, int portNumber, OSType protocol, OSType authType, NSData *passwordData, OSStatus (*funcOverride)(KCItemRef))
{
    NSMutableDictionary *attributesDictionary;
    SecKeychainAttributeList attributeList;
    OSStatus keychainStatus;
    KCItemRef item;
    UInt32 attributeIndex;
    
    if(funcOverride == NULL)
        funcOverride = addItemNoUIStub;
        
    attributesDictionary = [[NSMutableDictionary alloc] initWithCapacity:6];
    [attributesDictionary setObject:hostname forKey:@"server"];
    if (realm != nil) [attributesDictionary setObject:realm forKey:@"securityDomain"];
    if (username != nil) [attributesDictionary setObject:username forKey:@"account"];
    [attributesDictionary setIntValue:portNumber forKey:@"port"];
    [attributesDictionary setIntValue:protocol forKey:@"protocol"];
    [attributesDictionary setIntValue:authType forKey:@"authType"];
    attributeList.attr = KeychainAttributesFromDictionary(attributesDictionary, &attributeList.count);
    [attributesDictionary release];
    keychainStatus = KCNewItem(kInternetPasswordKCItemClass, 'OWEB', [passwordData length], [passwordData bytes], &item);
    if (keychainStatus != noErr)
        goto done;
    for (attributeIndex = 0; attributeIndex < attributeList.count; attributeIndex++) {
        keychainStatus = KCSetAttribute(item, &(attributeList.attr[attributeIndex]));
        if (keychainStatus != noErr)
            goto done;
    }
    NSLog(@"Will add item with attributes: %@", OWKCExtractKeyAttributes(item));
    keychainStatus = (*funcOverride)(item);
    KCReleaseItem(&item);
    if (keychainStatus == errKCDuplicateItem) {
        KCSearchRef searchState;

#warning Incorrect use of KCFindFirstItem()
        // we get a kcNosuchAttr error, which the docs say mean that the attributes aren't valid for this item class. We're not specifying an item class, so maybe that's the problem. Try specifying an item class in the attributes list, see if that helps anything
        keychainStatus = KCFindFirstItem(NULL, &attributeList, &searchState, &item);
        free(attributeList.attr);
        KCReleaseSearch(&searchState);
        if (keychainStatus != noErr)
            return keychainStatus;
        keychainStatus = KCSetData(item, [passwordData length], [passwordData bytes]);
        KCReleaseItem(&item);
        return keychainStatus;
    }
done:
    free(attributeList.attr);
    return keychainStatus;
}
