/*
 * Copyright (c) 2005 Apple Computer, 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@
 */

/*
 *  EncodedSourceID.c
 *  SourceIDValidation
 *
 *  (Uses FindPrimaryEthernetInterfaces() and GetPrimaryMACAddress() from developer.apple.com.)
 *  Copyright (c) 2005 Apple. All rights reserved.
 */

#include <CoreFoundation/CoreFoundation.h>

#include <IOKit/IOKitLib.h>
#include <IOKit/network/IOEthernetInterface.h>
#include <IOKit/network/IONetworkInterface.h>
#include <IOKit/network/IOEthernetController.h>

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <openssl/md5.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/buffer.h>

#include "EncodedSourceID.h"

#define ACCOUNT_NAME_MAX_LEN	255
#define ENCODED_ID_MAX_LEN	 31

#define SUCCESS 1
#define FAILURE 0

static int GenerateIDString(char *idBuffer);
static int PrimaryMACAddressFromSystem(char *addressBuffer);
static kern_return_t GetPrimaryMACAddress(UInt8 *MACAddress);
static kern_return_t FindPrimaryEthernetInterfaces(io_iterator_t *matchingServices);

/* Returns a Base-64 encoded MD5 hash of 'username:primary-mac-address' */
int GetEncodedSourceID(char encodedIdBuffer[32]) {

    char idStr[ACCOUNT_NAME_MAX_LEN + (kIOEthernetAddressSize * 2) + 2];
    unsigned char MD5Value[MD5_DIGEST_LENGTH];
    BIO *mbio,*b64bio,*bio;
    int b64Len = 0, writeLen = 0, status = FAILURE;
    BUF_MEM *bptr = NULL;
    
    if (encodedIdBuffer == NULL) return FAILURE;
    encodedIdBuffer[0] = '\0';
	
    if (GenerateIDString(idStr)) {

        /* Convert idStr to MD5 */
        MD5(idStr, strlen(idStr), MD5Value);
            
        /* Convert MD5 to Base-64 */
        mbio=BIO_new(BIO_s_mem());
        b64bio=BIO_new(BIO_f_base64());
        bio=BIO_push(b64bio,mbio);
        writeLen = BIO_write(bio,MD5Value,MD5_DIGEST_LENGTH);
        
        if (writeLen > 0) {
        
            BIO_flush(bio);
            BIO_get_mem_ptr(mbio, &bptr);
            if ( (bptr != NULL) && ((b64Len = bptr->length) > 0) ) {
                memcpy(encodedIdBuffer, bptr->data, b64Len);
                
                if (b64Len > ENCODED_ID_MAX_LEN) {
                    encodedIdBuffer[ENCODED_ID_MAX_LEN] = '\0';
                } else {
                    encodedIdBuffer[b64Len-1] = '\0'; /* Overwrites the newline char */
                }
                status = SUCCESS;
            }
        }
        BIO_free_all(bio);
    }
    
    return status;
}

static int GenerateIDString(char *idBuffer) {

    char addressString[(kIOEthernetAddressSize * 2) + 1];
    const char *accountName = NULL;

    if (idBuffer == NULL) return FAILURE;
    idBuffer[0] = '\0';
    
    accountName = getlogin();
                            
    if ( (accountName==NULL) || (strlen(accountName)<=0) ) {
    
        return FAILURE;
    }
	
    if (PrimaryMACAddressFromSystem(addressString) == SUCCESS) {
         
        sprintf(idBuffer, "%s:%s", accountName, addressString);
        return SUCCESS;
    }
    else {
        return FAILURE;
    }
}

/* Writes the MAC address of the primary interface to the addressBuffer argument as a 
 * human-readable hex string.
  */
static int PrimaryMACAddressFromSystem(char *addressBuffer) {

    int status = FAILURE, len = kIOEthernetAddressSize;
    UInt8 MACAddress[ kIOEthernetAddressSize ];
    kern_return_t kernResult = KERN_FAILURE;
    
    if (addressBuffer == NULL) return status;
	
    kernResult = GetPrimaryMACAddress(MACAddress);
                    
    if (KERN_SUCCESS == kernResult) {
    
            char *bufPtr = addressBuffer;
            const UInt8 *addrPtr = &MACAddress[0];
            
            while ( len-- ) {
                bufPtr += sprintf( bufPtr, "%2.2x", *addrPtr++ );
            }
    
            if (strlen(addressBuffer) > 0) {
              
                status = SUCCESS;
            }
    }

    return status;
}

/* -----------------------------------------------------------------------------
Get the mac address of the primary interface
----------------------------------------------------------------------------- */
static kern_return_t GetPrimaryMACAddress(UInt8 *MACAddress)
{
    io_object_t  intfService;
    io_object_t  controllerService;
    kern_return_t kernResult = KERN_FAILURE;
    io_iterator_t intfIterator;

    kernResult = FindPrimaryEthernetInterfaces(&intfIterator);
    if (kernResult != KERN_SUCCESS)
        return kernResult;

    // Initialize the returned address
    bzero(MACAddress, kIOEthernetAddressSize);
    
    // IOIteratorNext retains the returned object, so release it when we're done with it.
    while ((intfService = IOIteratorNext(intfIterator))) {
    
        CFTypeRef MACAddressAsCFData;        

        // IONetworkControllers can't be found directly by the IOServiceGetMatchingServices call, 
        // since they are hardware nubs and do not participate in driver matching. In other words,
        // registerService() is never called on them. So we've found the IONetworkInterface and will 
        // get its parent controller by asking for it specifically.
        
        // IORegistryEntryGetParentEntry retains the returned object, so release it when we're done with it.
        kernResult = IORegistryEntryGetParentEntry( intfService,
                                                    kIOServicePlane,
                                                    &controllerService );

        if (kernResult == KERN_SUCCESS) {
        
            // Retrieve the MAC address property from the I/O Registry in the form of a CFData
            MACAddressAsCFData = IORegistryEntryCreateCFProperty( controllerService,
                                                                  CFSTR(kIOMACAddress),
                                                                  kCFAllocatorDefault, 0);
            if (MACAddressAsCFData) {
                // Get the raw bytes of the MAC address from the CFData
                CFDataGetBytes(MACAddressAsCFData, CFRangeMake(0, kIOEthernetAddressSize), MACAddress);
                CFRelease(MACAddressAsCFData);
            }
                
            // Done with the parent Ethernet controller object so we release it.
            IOObjectRelease(controllerService);
        }
        
        // Done with the Ethernet interface object so we release it.
        IOObjectRelease(intfService);
    }
    
    IOObjectRelease(intfIterator);

    return kernResult;
}

/* -----------------------------------------------------------------------------
Returns an iterator containing the primary (built-in) Ethernet interface. 
The caller is responsible for releasing the iterator after the caller is done with it.
 ----------------------------------------------------------------------------- */
static kern_return_t FindPrimaryEthernetInterfaces(io_iterator_t *matchingServices)
{
    kern_return_t  kernResult; 
    mach_port_t   masterPort;
    CFMutableDictionaryRef matchingDict;
    CFMutableDictionaryRef propertyMatchDict;
    
    // Retrieve the Mach port used to initiate communication with I/O Kit
    kernResult = IOMasterPort(MACH_PORT_NULL, &masterPort);
    if (kernResult != KERN_SUCCESS) {
        //error("Can't create IOKit MasterPort (%d)\n", kernResult);
        return kernResult;
    }
    
    // Ethernet interfaces are instances of class kIOEthernetInterfaceClass. 
    // IOServiceMatching is a convenience function to create a dictionary with the key kIOProviderClassKey and 
    // the specified value.
    matchingDict = IOServiceMatching(kIOEthernetInterfaceClass);
    if (matchingDict) {
    
        // Each IONetworkInterface object has a Boolean property with the key kIOPrimaryInterface. Only the
        // primary (built-in) interface has this property set to TRUE.
        
        // IOServiceGetMatchingServices uses the default matching criteria defined by IOService. This considers
        // only the following properties plus any family-specific matching in this order of precedence 
        // (see IOService::passiveMatch):
        //
        // kIOProviderClassKey (IOServiceMatching)
        // kIONameMatchKey (IOServiceNameMatching)
        // kIOPropertyMatchKey
        // kIOPathMatchKey
        // kIOMatchedServiceCountKey
        // family-specific matching
        // kIOBSDNameKey (IOBSDNameMatching)
        // kIOLocationMatchKey
        
        // The IONetworkingFamily does not define any family-specific matching. This means that in            
        // order to have IOServiceGetMatchingServices consider the kIOPrimaryInterface property, we must
        // add that property to a separate dictionary and then add that to our matching dictionary
        // specifying kIOPropertyMatchKey.
            
        propertyMatchDict = CFDictionaryCreateMutable( kCFAllocatorDefault, 0,
                                                       &kCFTypeDictionaryKeyCallBacks,
                                                       &kCFTypeDictionaryValueCallBacks);
        if (propertyMatchDict) {
        
            // Set the value in the dictionary of the property with the given key, or add the key 
            // to the dictionary if it doesn't exist. This call retains the value object passed in.
            CFDictionarySetValue(propertyMatchDict, CFSTR(kIOPrimaryInterface), kCFBooleanTrue); 
            
            // Now add the dictionary containing the matching value for kIOPrimaryInterface to our main
            // matching dictionary. This call will retain propertyMatchDict, so we can release our reference 
            // on propertyMatchDict after adding it to matchingDict.
            CFDictionarySetValue(matchingDict, CFSTR(kIOPropertyMatchKey), propertyMatchDict);
            CFRelease(propertyMatchDict);
        }
    }
    
    // IOServiceGetMatchingServices retains the returned iterator, so release the iterator when we're done with it.
    // IOServiceGetMatchingServices also consumes a reference on the matching dictionary so we don't need to release
    // the dictionary explicitly.
    kernResult = IOServiceGetMatchingServices(masterPort, matchingDict, matchingServices);    
        
    return kernResult;
}